From c1502b3c1fe8feaaf0a65eb04ea93cfea314a8c2 Mon Sep 17 00:00:00 2001 From: Zhomart Mukhamejanov Date: Sun, 17 Jul 2022 23:42:45 -0700 Subject: [PATCH] examples: add macos_tray example app (#15101) --- cmd/tools/modules/testing/common.v | 3 + examples/macos_tray/icon.png | Bin 0 -> 690 bytes examples/macos_tray/tray.m | 131 +++++++++++++++++++++++++++++ examples/macos_tray/tray.v | 72 ++++++++++++++++ examples/macos_tray/v.mod | 0 5 files changed, 206 insertions(+) create mode 100644 examples/macos_tray/icon.png create mode 100644 examples/macos_tray/tray.m create mode 100644 examples/macos_tray/tray.v create mode 100644 examples/macos_tray/v.mod diff --git a/cmd/tools/modules/testing/common.v b/cmd/tools/modules/testing/common.v index db81b5eb36..ed3dfa8c25 100644 --- a/cmd/tools/modules/testing/common.v +++ b/cmd/tools/modules/testing/common.v @@ -198,6 +198,9 @@ pub fn new_test_session(_vargs string, will_compile bool) TestSession { skip_files << 'examples/sokol/sounds/wav_player.v' skip_files << 'examples/sokol/sounds/simple_sin_tones.v' } + $if !macos { + skip_files << 'examples/macos_tray/tray.v' + } } vargs := _vargs.replace('-progress', '').replace('-progress', '') vexe := pref.vexe_path() diff --git a/examples/macos_tray/icon.png b/examples/macos_tray/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..46233bc549ba26a11f13d680e9f5ad15af3c39ff GIT binary patch literal 690 zcmV;j0!{siP)Px%Y)M2xR7gu>WS|jXq>;G{L)>8Z%5{$!8JU>AzI%56)APHR-#@!~?$PNjv#|x2 zyrxA8zqqt9udt*pC!et5|NsB~?p@F<2UCj@6iVhnS$cs*Q;`k2v1dW!jXiT3ki}F? zUF$T=JsOe4t{z;Md;jv$DOe3=U|?Vl=s5BRS;OByzkY14apFW4^UUb@#>~QsBDQ5p zF)ITD16(ggMQc zbGJ(9+kvd{!Ksb2F0AWL2Z?Fh1x;4ev(7=5Ke>6P*XxJ3wj+ySO>O}lNB$!#`v33m z-;GuFEFdxOtnS~8OiW-l3~ZTFjA0H|gYB|r921h$u|(Fqv&&DG@yCz9PD#zrkj3A> ze0bvM>PaYSLGoA)=8=%oaw%AL71@C2R}XFY{p$KpDOH1TWbp&bx^=$1dx4@Br@%Aik>};S^ z1p&`*pI?4<=Moe(Fge_11uw5iK=t<5$OixZ{F;IB<2^*AEvOe``0@os4Pk>B7#RFo z_WfXDVPi)&{MW;y3{1={4FCTA`MGU+IR~;F!Q=*0py86#py85KkF4R_gA)vFEQ|~{ zPw#5JeR5|VSq3vOFmMEP9Qlr{`Oo){3=D5?Fl?Dp49e0!$TgV3t9Hw4Hf{kCWCOn3 z*!gzPf@TykbPEU;^q@4Y;gHk})A*l}f$_o7mDAony1oaOYw@OIe42=nBj{pc48f)b Y08lK|7g~P<0ssI207*qoM6N<$f`-pb{r~^~ literal 0 HcmV?d00001 diff --git a/examples/macos_tray/tray.m b/examples/macos_tray/tray.m new file mode 100644 index 0000000000..d24191baf3 --- /dev/null +++ b/examples/macos_tray/tray.m @@ -0,0 +1,131 @@ +// #include // Uncomment for C/C++ intellisense + +// See this tutorial to learn about NSApplicationDelegate: +// https://bumbershootsoft.wordpress.com/2018/11/22/unfiltered-cocoa-completing-the-application/ + +static NSString *nsstring(string s) { + return [[NSString alloc] initWithBytesNoCopy:s.str + length:s.len + encoding:NSUTF8StringEncoding + freeWhenDone:NO]; +} + +// Manages the app lifecycle. +@interface AppDelegate : NSObject { + NSAutoreleasePool *pool; + NSApplication *app; + NSStatusItem *statusItem; // our button + main__TrayParams trayParams; // TrayParams is defined in tray.v +} +@end + +@implementation AppDelegate +- (AppDelegate *)initWithParams:(main__TrayParams)params { + if (self = [super init]) { + trayParams = params; + } + return self; +} + +// Called when NSMenuItem is clicked. +- (void)onAction:(id)sender { + struct main__TrayMenuItem *item = + (struct main__TrayMenuItem *)[[sender representedObject] pointerValue]; + if (item) { + trayParams.on_click(*item); + } +} + +- (NSMenu *)buildMenu { + NSMenu *menu = [NSMenu new]; + [menu autorelease]; + [menu setAutoenablesItems:NO]; + + main__TrayMenuItem *params_items = trayParams.items.data; + for (int i = 0; i < trayParams.items.len; i++) { + NSString *title = nsstring(params_items[i].text); + NSMenuItem *item = [menu addItemWithTitle:title + action:@selector(onAction:) + keyEquivalent:@""]; + NSValue *representedObject = [NSValue valueWithPointer:(params_items + i)]; + [item setRepresentedObject:representedObject]; + [item setTarget:self]; + [item autorelease]; + [item setEnabled:YES]; + } + + return menu; +} + +- (void)initTrayMenuItem { + NSStatusBar *statusBar = [NSStatusBar systemStatusBar]; + statusItem = [statusBar statusItemWithLength:NSSquareStatusItemLength]; + [statusItem retain]; + [statusItem setVisible:YES]; + NSStatusBarButton *statusBarButton = [statusItem button]; + + // Height must be 22px. + NSImage *img = [NSImage imageNamed:@"icon.png"]; + [statusBarButton setImage:img]; + NSMenu *menu = [self buildMenu]; + [statusItem setMenu:menu]; +} + +- (void)applicationWillFinishLaunching:(NSNotification *)notification { + NSLog(@"applicationWillFinishLaunching called"); +} + +- (void)applicationWillTerminate:(NSNotification *)notif; +{ NSLog(@"applicationWillTerminate called"); } + +- (NSApplicationTerminateReply)applicationShouldTerminate: + (NSApplication *)sender { + NSLog(@"applicationShouldTerminate called"); + return NSTerminateNow; +} +@end + +// Initializes NSApplication and NSStatusItem, aka system tray menu item. +main__TrayInfo *tray_app_init(main__TrayParams params) { + NSApplication *app = [NSApplication sharedApplication]; + AppDelegate *appDelegate = [[AppDelegate alloc] initWithParams:params]; + + // Hide icon from the doc. + [app setActivationPolicy:NSApplicationActivationPolicyProhibited]; + [app setDelegate:appDelegate]; + + [appDelegate initTrayMenuItem]; + + main__TrayInfo *tray_info = malloc(sizeof(main__TrayInfo)); + tray_info->app = app; + tray_info->app_delegate = appDelegate; + return tray_info; +} + +// Blocks and runs the application. +void tray_app_run(main__TrayInfo *tray_info) { + NSApplication *app = (NSApplication *)(tray_info->app); + [app run]; +} + +// Processes a single NSEvent while blocking the thread +// until there is an event. +void tray_app_loop(main__TrayInfo *tray_info) { + NSDate *until = [NSDate distantFuture]; + + NSApplication *app = (NSApplication *)(tray_info->app); + NSEvent *event = [app nextEventMatchingMask:ULONG_MAX + untilDate:until + inMode:@"kCFRunLoopDefaultMode" + dequeue:YES]; + + if (event) { + [app sendEvent:event]; + } +} + +// Terminates the app. +void tray_app_exit(main__TrayInfo *tray_info) { + NSApplication *app = (NSApplication *)(tray_info->app); + [app terminate:app]; +} diff --git a/examples/macos_tray/tray.v b/examples/macos_tray/tray.v new file mode 100644 index 0000000000..261baddb61 --- /dev/null +++ b/examples/macos_tray/tray.v @@ -0,0 +1,72 @@ +// Simple windows-less application that shows a icon button +// on Mac OS tray. +// +// Tested on Mac OS Monterey (12.3). + +module main + +#include +#flag -framework Cocoa + +#include "@VMODROOT/tray.m" + +fn C.tray_app_init(TrayParams) &TrayInfo +fn C.tray_app_loop(&TrayInfo) +fn C.tray_app_run(&TrayInfo) +fn C.tray_app_exit(&TrayInfo) + +struct TrayMenuItem { + id string [required] // Unique ID. + text string [required] // Text to display. +} + +// Parameters to configure the tray button. +struct TrayParams { + items []TrayMenuItem [required] + on_click fn (item TrayMenuItem) +} + +// Internal Cocoa application state. +struct TrayInfo { + app voidptr // pointer to NSApplication + app_delegate voidptr // pointer to AppDelegate +} + +[heap] +struct MyApp { +mut: + tray_info &TrayInfo +} + +fn (app &MyApp) on_menu_item_click(item TrayMenuItem) { + println('click $item.id') + if item.id == 'quit' { + C.tray_app_exit(app.tray_info) + } +} + +fn main() { + mut my_app := &MyApp{ + tray_info: 0 + } + + my_app.tray_info = C.tray_app_init(TrayParams{ + items: [TrayMenuItem{ + id: 'hello' + text: 'Hello' + }, TrayMenuItem{ + id: 'quit' + text: 'Quit!' + }] + on_click: my_app.on_menu_item_click + }) + + //// Use this: + // for { + // C.tray_app_loop(my_app.tray_info) + // // println("loop") + // } + + //// Or this: + C.tray_app_run(my_app.tray_info) +} diff --git a/examples/macos_tray/v.mod b/examples/macos_tray/v.mod new file mode 100644 index 0000000000..e69de29bb2