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 0000000000..46233bc549 Binary files /dev/null and b/examples/macos_tray/icon.png differ 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