diff --git a/index.d.ts b/index.d.ts index bd3cb11..bd622a9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -76,6 +76,19 @@ export class Menu { */ constructor(items: ReadonlyArray); + /** + * Open the menu and return a promise resolved when a selection is made by the user. + * Should be called in response to `NotifyIcon#onSelect()`, which + * will give the `mouseX`, `mouseY` values to use for the arguments. + * If called at other times, you will likely not have a foreground + * window, which will cause the menu to misbehave, not correctly closing + * on the first selection. + * @param x Desktop x coordinate to open menu near. + * @param y Desktop y coordinate to open menu near. + * @returns Item id if selected or `null` if the menu was dismissed. + */ + show(x: number, y: number): Promise; + /** * Open the menu and block until a selection is made by the user. * Should be called in response to `NotifyIcon#onSelect()`, which diff --git a/src/menu-object.cc b/src/menu-object.cc index 906a2a5..5d672b7 100644 --- a/src/menu-object.cc +++ b/src/menu-object.cc @@ -217,6 +217,64 @@ napi_value export_Menu_createFromTemplate(napi_env env, return wrap_menu(env, load_menu_indirect(env, data)); } +napi_value export_Menu_show(napi_env env, napi_callback_info info) { + MenuObject* this_object; + int32_t mouse_x; + int32_t mouse_y; + NAPI_RETURN_NULL_IF_NOT_OK(napi_get_cb_info(env, info, &this_object, nullptr, + 2, &mouse_x, &mouse_y)); + + auto env_data = get_env_data(env); + if (!env_data) { + return nullptr; + } + + HMENU menu = this_object->menu; + + napi_deferred deferred; + napi_value promise; + NAPI_THROW_RETURN_NULL_IF_NOT_OK( + env, napi_create_promise(env, &deferred, &promise)); + + env_data->icon_message_loop.run_on_msg_thread_nonblocking([=] { + auto env_data = get_env_data(env); + if (!env_data) { + return; + } + + int32_t item_id = 0; + DWORD error = 0; + item_id = (int32_t)TrackPopupMenuEx( + menu, + GetSystemMetrics(SM_MENUDROPALIGNMENT) | TPM_RETURNCMD | TPM_NONOTIFY, + mouse_x, mouse_y, env_data->icon_message_loop.hwnd, nullptr); + if (!item_id) { + error = GetLastError(); + } + + env_data->icon_message_loop.run_on_env_thread.blocking( + [=](napi_env env, napi_value) { + if (error) { + napi_value error_value; + NAPI_THROW_RETURN_VOID_IF_NOT_OK( + env, napi_create_win32_error(env, "TrackPopupMenuEx", error, + &error_value)); + NAPI_THROW_RETURN_VOID_IF_NOT_OK( + env, napi_reject_deferred(env, deferred, error_value)); + } else { + napi_value result; + NAPI_THROW_RETURN_VOID_IF_NOT_OK( + env, item_id ? napi_create(env, item_id, &result) + : napi_get_null(env, &result)); + NAPI_THROW_RETURN_VOID_IF_NOT_OK( + env, napi_resolve_deferred(env, deferred, result)); + } + }); + }); + + return promise; +} + napi_value export_Menu_showSync(napi_env env, napi_callback_info info) { MenuObject* this_object; int32_t mouse_x; @@ -228,7 +286,7 @@ napi_value export_Menu_showSync(napi_env env, napi_callback_info info) { HMENU menu = this_object->menu; - int item_id = 0; + int32_t item_id = 0; DWORD error = 0; env_data->icon_message_loop.run_on_msg_thread_blocking([=, &item_id, &error] { @@ -246,7 +304,9 @@ napi_value export_Menu_showSync(napi_env env, napi_callback_info info) { return nullptr; } napi_value result; - NAPI_THROW_RETURN_NULL_IF_NOT_OK(env, napi_create(env, item_id, &result)); + NAPI_THROW_RETURN_NULL_IF_NOT_OK(env, item_id + ? napi_create(env, item_id, &result) + : napi_get_null(env, &result)); return result; } @@ -364,6 +424,7 @@ auto MenuObject::define_class(EnvData* env_data, napi_value* constructor_value) return NapiWrapped::define_class( env_data->env, "Menu", constructor_value, &env_data->menu_constructor, { + napi_method_property("show", export_Menu_show), napi_method_property("showSync", export_Menu_showSync), napi_method_property("getAt", export_Menu_getAt), napi_method_property("get", export_Menu_get), diff --git a/test/index.ts b/test/index.ts index 0598ee0..75debb3 100644 --- a/test/index.ts +++ b/test/index.ts @@ -66,10 +66,10 @@ async function main() { // tooltip: "Will get garbage collected", // }); - const onSelect = catchErrors(function (this: NotifyIcon, event: NotifyIcon.SelectEvent) { + const onSelect = catchErrorsAsync(async function (this: NotifyIcon, event: NotifyIcon.SelectEvent) { console.log("tray icon selected %O %O", this, event); - const itemId = contextMenu.showSync(event.mouseX, event.mouseY); + const itemId = await contextMenu.show(event.mouseX, event.mouseY); console.log("menu item selected %O", itemId); if (!itemId) { return; @@ -82,7 +82,7 @@ async function main() { process.exit(0); return; case 2: - throw new TestError("Should bubble out to uncaughtException listener."); + throw new TestError("Should bubble out to unhandledRejection listener."); case 3: new NotifyIcon({ icon: Icon.load(Icon.ids.info, Icon.small),