返回 DeepSeek-Reasonix
systray_darwin.m
根目录 / desktop / third_party / systray / systray_darwin.m
1 //go:build !ios
2
3 #import <Cocoa/Cocoa.h>
4 #include "systray.h"
5
6 #if __MAC_OS_X_VERSION_MIN_REQUIRED < 101400
7
8 #ifndef NSControlStateValueOff
9 #define NSControlStateValueOff NSOffState
10 #endif
11
12 #ifndef NSControlStateValueOn
13 #define NSControlStateValueOn NSOnState
14 #endif
15
16 #endif
17
18 // The modifier bits used by the Go API, converted to NSEventModifierFlags below.
19 #define SYSTRAY_MOD_SHIFT (1 << 0)
20 #define SYSTRAY_MOD_CONTROL (1 << 1)
21 #define SYSTRAY_MOD_ALT (1 << 2)
22 #define SYSTRAY_MOD_SUPER (1 << 3)
23
24 @interface MenuItem : NSObject
25 {
26 @public
27 NSNumber* menuId;
28 NSNumber* parentMenuId;
29 NSString* title;
30 NSString* tooltip;
31 NSString* shortcutKey;
32 NSEventModifierFlags shortcutMods;
33 short disabled;
34 short checked;
35 }
36 -(id) initWithId: (int)theMenuId
37 withParentMenuId: (int)theParentMenuId
38 withTitle: (const char*)theTitle
39 withTooltip: (const char*)theTooltip
40 withShortcutKey: (const char*)theShortcutKey
41 withShortcutMods: (unsigned int)theShortcutMods
42 withDisabled: (short)theDisabled
43 withChecked: (short)theChecked;
44 @end
45 @implementation MenuItem
46 -(id) initWithId: (int)theMenuId
47 withParentMenuId: (int)theParentMenuId
48 withTitle: (const char*)theTitle
49 withTooltip: (const char*)theTooltip
50 withShortcutKey: (const char*)theShortcutKey
51 withShortcutMods: (unsigned int)theShortcutMods
52 withDisabled: (short)theDisabled
53 withChecked: (short)theChecked
54 {
55 menuId = [NSNumber numberWithInt:theMenuId];
56 parentMenuId = [NSNumber numberWithInt:theParentMenuId];
57 title = [[NSString alloc] initWithCString:theTitle
58 encoding:NSUTF8StringEncoding];
59 tooltip = [[NSString alloc] initWithCString:theTooltip
60 encoding:NSUTF8StringEncoding];
61 shortcutKey = [[NSString alloc] initWithCString:theShortcutKey
62 encoding:NSUTF8StringEncoding];
63 shortcutMods = 0;
64 if (theShortcutMods & SYSTRAY_MOD_SHIFT) {
65 shortcutMods |= NSEventModifierFlagShift;
66 }
67 if (theShortcutMods & SYSTRAY_MOD_CONTROL) {
68 shortcutMods |= NSEventModifierFlagControl;
69 }
70 if (theShortcutMods & SYSTRAY_MOD_ALT) {
71 shortcutMods |= NSEventModifierFlagOption;
72 }
73 if (theShortcutMods & SYSTRAY_MOD_SUPER) {
74 shortcutMods |= NSEventModifierFlagCommand;
75 }
76 disabled = theDisabled;
77 checked = theChecked;
78 return self;
79 }
80 @end
81
82 @interface RightClickDetector : NSView
83
84 @property (copy) void (^onRightClicked)(NSEvent *);
85
86 @end
87
88 @implementation RightClickDetector
89
90 - (void)rightMouseUp:(NSEvent *)theEvent {
91 if (!self.onRightClicked) {
92 return;
93 }
94
95 self.onRightClicked(theEvent);
96 }
97
98 @end
99
100
101 @interface SystrayAppDelegate: NSObject <NSApplicationDelegate, NSMenuDelegate>
102 - (void) add_or_update_menu_item:(MenuItem*) item;
103 - (IBAction)menuHandler:(id)sender;
104 - (void)menuWillOpen:(NSMenu*)menu;
105 @property (assign) IBOutlet NSWindow *window;
106 @end
107
108 @implementation SystrayAppDelegate
109 {
110 NSStatusItem *statusItem;
111 NSMenu *menu;
112 NSCondition* cond;
113 }
114
115 @synthesize window = _window;
116
117 - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
118 {
119 self->statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
120
121 self->menu = [[NSMenu alloc] init];
122 self->menu.delegate = self;
123 self->menu.autoenablesItems = FALSE;
124 // Once the user has removed it, the item needs to be explicitly brought back,
125 // even restarting the application is insufficient.
126 // Since the interface from Go is relatively simple, for now we ensure it's
127 // always visible at application startup.
128 self->statusItem.visible = TRUE;
129
130 NSStatusBarButton *button = self->statusItem.button;
131 button.action = @selector(leftMouseClicked);
132
133 [NSEvent addLocalMonitorForEventsMatchingMask: (NSEventTypeLeftMouseDown|NSEventTypeRightMouseDown)
134 handler: ^NSEvent *(NSEvent *event) {
135 if (event.window != self->statusItem.button.window) {
136 return event;
137 }
138
139 if (event.modifierFlags & NSEventModifierFlagCommand) {
140 return event;
141 }
142
143 [self leftMouseClicked];
144
145 return nil;
146 }];
147
148 NSSize size = [button frame].size;
149 NSRect frame = CGRectMake(0, 0, size.width, size.height);
150 RightClickDetector *rightClicker = [[RightClickDetector alloc] initWithFrame:frame];
151 rightClicker.onRightClicked = ^(NSEvent *event) {
152 [self rightMouseClicked];
153 };
154
155 rightClicker.autoresizingMask = (NSViewWidthSizable |
156 NSViewHeightSizable);
157 button.autoresizesSubviews = YES;
158 [button addSubview:rightClicker];
159
160 systray_ready();
161 }
162
163 - (void)rightMouseClicked {
164 systray_right_click();
165 }
166
167 - (void)leftMouseClicked {
168 systray_left_click();
169 }
170
171 - (void)applicationWillTerminate:(NSNotification *)aNotification
172 {
173 systray_on_exit();
174 }
175
176 - (void)setRemovalAllowed {
177 NSStatusItemBehavior behavior = [self->statusItem behavior];
178 behavior |= NSStatusItemBehaviorRemovalAllowed;
179 self->statusItem.behavior = behavior;
180 }
181
182 - (void)setRemovalForbidden {
183 NSStatusItemBehavior behavior = [self->statusItem behavior];
184 behavior &= ~NSStatusItemBehaviorRemovalAllowed;
185 // Ensure the menu item is visible if it was removed, since we're now
186 // disallowing removal.
187 self->statusItem.visible = TRUE;
188 self->statusItem.behavior = behavior;
189 }
190
191 - (void)setIcon:(NSImage *)image {
192 statusItem.button.image = image;
193 [self updateTitleButtonStyle];
194 }
195
196 - (void)setTitle:(NSString *)title {
197 statusItem.button.title = title;
198 [self updateTitleButtonStyle];
199 }
200
201 - (void)updateTitleButtonStyle {
202 if (statusItem.button.image != nil) {
203 if ([statusItem.button.title length] == 0) {
204 statusItem.button.imagePosition = NSImageOnly;
205 } else {
206 statusItem.button.imagePosition = NSImageLeft;
207 }
208 } else {
209 statusItem.button.imagePosition = NSNoImage;
210 }
211 }
212
213
214 - (void)setTooltip:(NSString *)tooltip {
215 statusItem.button.toolTip = tooltip;
216 }
217
218 - (IBAction)menuHandler:(id)sender
219 {
220 NSNumber* menuId = [sender representedObject];
221 systray_menu_item_selected(menuId.intValue);
222 }
223
224 - (void)menuWillOpen:(NSMenu *)menu {
225 systray_menu_will_open();
226 }
227
228 - (void)add_or_update_menu_item:(MenuItem *)item {
229 NSMenu *theMenu = self->menu;
230 NSMenuItem *parentItem;
231 if ([item->parentMenuId integerValue] > 0) {
232 parentItem = find_menu_item(menu, item->parentMenuId);
233 if (parentItem.hasSubmenu) {
234 theMenu = parentItem.submenu;
235 } else {
236 theMenu = [[NSMenu alloc] init];
237 [theMenu setAutoenablesItems:NO];
238 [parentItem setSubmenu:theMenu];
239 }
240 }
241
242 NSMenuItem *menuItem = find_menu_item(theMenu, item->menuId);
243 if (menuItem == NULL) {
244 menuItem = [theMenu addItemWithTitle:item->title
245 action:@selector(menuHandler:)
246 keyEquivalent:@""];
247 [menuItem setRepresentedObject:item->menuId];
248 }
249 [menuItem setTitle:item->title];
250 [menuItem setTag:[item->menuId integerValue]];
251 [menuItem setTarget:self];
252 [menuItem setToolTip:item->tooltip];
253 [menuItem setKeyEquivalent:item->shortcutKey];
254 [menuItem setKeyEquivalentModifierMask:item->shortcutMods];
255 if (item->disabled == 1) {
256 menuItem.enabled = FALSE;
257 } else {
258 menuItem.enabled = TRUE;
259 }
260 if (item->checked == 1) {
261 menuItem.state = NSControlStateValueOn;
262 } else {
263 menuItem.state = NSControlStateValueOff;
264 }
265 }
266
267 NSMenuItem *find_menu_item(NSMenu *ourMenu, NSNumber *menuId) {
268 NSMenuItem *foundItem = [ourMenu itemWithTag:[menuId integerValue]];
269 if (foundItem != NULL) {
270 return foundItem;
271 }
272 NSArray *menu_items = ourMenu.itemArray;
273 int i;
274 for (i = 0; i < [menu_items count]; i++) {
275 NSMenuItem *i_item = [menu_items objectAtIndex:i];
276 if (i_item.hasSubmenu) {
277 foundItem = find_menu_item(i_item.submenu, menuId);
278 if (foundItem != NULL) {
279 return foundItem;
280 }
281 }
282 }
283
284 return NULL;
285 };
286
287 - (void) add_separator:(NSNumber*) parentMenuId
288 {
289 if (parentMenuId.integerValue != 0) {
290 NSMenuItem* menuItem = find_menu_item(menu, parentMenuId);
291 if (menuItem != NULL) {
292 [menuItem.submenu addItem: [NSMenuItem separatorItem]];
293 return;
294 }
295 }
296 [menu addItem: [NSMenuItem separatorItem]];
297 }
298
299 - (void) hide_menu_item:(NSNumber*) menuId
300 {
301 NSMenuItem* menuItem = find_menu_item(menu, menuId);
302 if (menuItem != NULL) {
303 [menuItem setHidden:TRUE];
304 }
305 }
306
307 - (void) setMenuItemIcon:(NSArray*)imageAndMenuId {
308 NSImage* image = [imageAndMenuId objectAtIndex:0];
309 NSNumber* menuId = [imageAndMenuId objectAtIndex:1];
310
311 NSMenuItem* menuItem;
312 menuItem = find_menu_item(menu, menuId);
313 if (menuItem == NULL) {
314 return;
315 }
316 menuItem.image = image;
317 }
318
319 - (void)show_menu
320 {
321 // Attach the menu and synthesize a click so AppKit positions it natively,
322 // then detach it in menuDidClose: so the next click reaches the button
323 // action (and the Go tap handlers) again.
324 self->statusItem.menu = self->menu;
325 [self->statusItem.button performClick:nil];
326 }
327
328 - (void)menuDidClose:(NSMenu *)menu {
329 // Defer the detach so we don't pull the menu out from under AppKit
330 // while it is still tearing the menu down.
331 dispatch_async(dispatch_get_main_queue(), ^{
332 self->statusItem.menu = nil;
333 });
334 }
335
336 - (void) show_menu_item:(NSNumber*) menuId
337 {
338 NSMenuItem* menuItem = find_menu_item(menu, menuId);
339 if (menuItem != NULL) {
340 [menuItem setHidden:FALSE];
341 }
342 }
343
344 - (void) remove_menu_item:(NSNumber*) menuId
345 {
346 NSMenuItem* menuItem = find_menu_item(menu, menuId);
347 if (menuItem != NULL) {
348 [menuItem.menu removeItem:menuItem];
349 }
350 }
351
352 - (void) reset_menu
353 {
354 [self->menu removeAllItems];
355 }
356
357 - (void) quit
358 {
359 // This tells the app event loop to stop after processing remaining messages.
360 [NSApp stop:self];
361 // The event loop won't return until it processes another event.
362 // https://stackoverflow.com/a/48064752/149482
363 NSPoint eventLocation = NSMakePoint(0, 0);
364 NSEvent *customEvent = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
365 location:eventLocation
366 modifierFlags:0
367 timestamp:0
368 windowNumber:0
369 context:nil
370 subtype:0
371 data1:0
372 data2:0];
373 [NSApp postEvent:customEvent atStart:NO];
374 }
375
376 @end
377
378 bool internalLoop = false;
379 SystrayAppDelegate *owner;
380
381 void setInternalLoop(bool i) {
382 internalLoop = i;
383 }
384
385 void registerSystray(void) {
386 if (!internalLoop) { // with an external loop we don't take ownership of the app
387 return;
388 }
389
390 owner = [[SystrayAppDelegate alloc] init];
391 [[NSApplication sharedApplication] setDelegate:owner];
392
393 // A workaround to avoid crashing on macOS versions before Catalina. Somehow
394 // SIGSEGV would happen inside AppKit if [NSApp run] is called from a
395 // different function, even if that function is called right after this.
396 if (floor(NSAppKitVersionNumber) <= /*NSAppKitVersionNumber10_14*/ 1671){
397 [NSApp run];
398 }
399 }
400
401 void nativeEnd(void) {
402 systray_on_exit();
403 }
404
405 int nativeLoop(void) {
406 if (floor(NSAppKitVersionNumber) > /*NSAppKitVersionNumber10_14*/ 1671){
407 [NSApp run];
408 }
409 return EXIT_SUCCESS;
410 }
411
412 void nativeStart(void) {
413 owner = [[SystrayAppDelegate alloc] init];
414
415 NSNotification *launched = [NSNotification notificationWithName:NSApplicationDidFinishLaunchingNotification
416 object:[NSApplication sharedApplication]];
417 [owner applicationDidFinishLaunching:launched];
418 }
419
420 void runInMainThread(SEL method, id object) {
421 [owner
422 performSelectorOnMainThread:method
423 withObject:object
424 waitUntilDone: YES];
425 }
426
427 void setIcon(const char* iconBytes, int length, bool template) {
428 NSData* buffer = [NSData dataWithBytes: iconBytes length:length];
429 @autoreleasepool {
430 NSImage *image = [[NSImage alloc] initWithData:buffer];
431 [image setSize:NSMakeSize(16, 16)];
432 image.template = template;
433 runInMainThread(@selector(setIcon:), (id)image);
434 }
435 }
436
437 void setMenuItemIcon(const char* iconBytes, int length, int menuId, bool template) {
438 NSData* buffer = [NSData dataWithBytes: iconBytes length:length];
439 @autoreleasepool {
440 NSImage *image = [[NSImage alloc] initWithData:buffer];
441 [image setSize:NSMakeSize(16, 16)];
442 image.template = template;
443 NSNumber *mId = [NSNumber numberWithInt:menuId];
444 runInMainThread(@selector(setMenuItemIcon:), @[image, (id)mId]);
445 }
446 }
447
448 void setTitle(char* ctitle) {
449 NSString* title = [[NSString alloc] initWithCString:ctitle
450 encoding:NSUTF8StringEncoding];
451 free(ctitle);
452 runInMainThread(@selector(setTitle:), (id)title);
453 }
454
455 void setTooltip(char* ctooltip) {
456 NSString* tooltip = [[NSString alloc] initWithCString:ctooltip
457 encoding:NSUTF8StringEncoding];
458 free(ctooltip);
459 runInMainThread(@selector(setTooltip:), (id)tooltip);
460 }
461
462 void setRemovalAllowed(bool allowed) {
463 if (allowed) {
464 runInMainThread(@selector(setRemovalAllowed), nil);
465 } else {
466 runInMainThread(@selector(setRemovalForbidden), nil);
467 }
468 }
469
470 void add_or_update_menu_item(int menuId, int parentMenuId, char* title, char* tooltip, char* shortcutKey, unsigned int shortcutMods, short disabled, short checked, short isCheckable) {
471 MenuItem* item = [[MenuItem alloc] initWithId: menuId withParentMenuId: parentMenuId withTitle: title withTooltip: tooltip withShortcutKey: shortcutKey withShortcutMods: shortcutMods withDisabled: disabled withChecked: checked];
472 free(title);
473 free(tooltip);
474 free(shortcutKey);
475 runInMainThread(@selector(add_or_update_menu_item:), (id)item);
476 }
477
478 void add_separator(int menuId, int parentId) {
479 NSNumber *pId = [NSNumber numberWithInt:parentId];
480 runInMainThread(@selector(add_separator:), (id)pId);
481 }
482
483 void hide_menu_item(int menuId) {
484 NSNumber *mId = [NSNumber numberWithInt:menuId];
485 runInMainThread(@selector(hide_menu_item:), (id)mId);
486 }
487
488 void remove_menu_item(int menuId) {
489 NSNumber *mId = [NSNumber numberWithInt:menuId];
490 runInMainThread(@selector(remove_menu_item:), (id)mId);
491 }
492
493 void show_menu() {
494 runInMainThread(@selector(show_menu), nil);
495 }
496
497 void show_menu_item(int menuId) {
498 NSNumber *mId = [NSNumber numberWithInt:menuId];
499 runInMainThread(@selector(show_menu_item:), (id)mId);
500 }
501
502 void reset_menu() {
503 runInMainThread(@selector(reset_menu), nil);
504 }
505
506 void quit() {
507 runInMainThread(@selector(quit), nil);
508 }
509
509 lines Plain Text