From b19fd48316567aa7f7a84d66a2214233cf099246 Mon Sep 17 00:00:00 2001 From: Soar Qin Date: Thu, 28 Oct 2021 21:22:50 +0800 Subject: [PATCH] put a tray icon and remove program tab from taskbar --- README.md | 1 + src/CMakeLists.txt | 2 +- src/D2RMH.ico | Bin 0 -> 16446 bytes src/main.cpp | 26 +++- src/res.rc | 1 + src/tray/LICENSE | 21 +++ src/tray/README.md | 146 ++++++++++++++++++ src/tray/tray.h | 370 +++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 562 insertions(+), 5 deletions(-) create mode 100644 src/D2RMH.ico create mode 100644 src/res.rc create mode 100644 src/tray/LICENSE create mode 100644 src/tray/README.md create mode 100644 src/tray/tray.h diff --git a/README.md b/README.md index 03c3d18..39182a7 100644 --- a/README.md +++ b/README.md @@ -54,3 +54,4 @@ Diablo II Resurrected map revealing tool. * [inih](https://github.com/benhoyt/inih) for reading INI files. * [JSON for Modern C++](https://github.com/nlohmann/json) for reading JSON files. * [CascLib](https://github.com/ladislav-zezula/CascLib) for reading Casc Storage from Diablo II Resurrected. +* [tray](https://github.com/zserge/tray) for tray icon support diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7d733bb..5ff8596 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,4 +1,4 @@ -file(GLOB D2RMH_SRC_FILES *.cpp *.h) +file(GLOB D2RMH_SRC_FILES *.cpp *.h *.rc) add_executable(D2RMH ${D2RMH_SRC_FILES}) target_include_directories(D2RMH PRIVATE ../d2mapapi) target_link_libraries(D2RMH inih d2mapapi shlwapi) diff --git a/src/D2RMH.ico b/src/D2RMH.ico new file mode 100644 index 0000000000000000000000000000000000000000..9ac76ac5459b0fd4c22dbe1fda039c8af09d21d8 GIT binary patch literal 16446 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 ZVPRn*xl>r!>Ijc7O!E0+qkeAu{|l$7QiT8j literal 0 HcmV?d00001 diff --git a/src/main.cpp b/src/main.cpp index 1780898..cc811dc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,8 @@ #include "d2rprocess.h" #include "cfg.h" #include "ini.h" +#define TRAY_WINAPI 1 +#include "tray/tray.h" #include "../common/jsonlng.h" #include "sokol/HandmadeMath.h" @@ -360,17 +362,32 @@ static void initSokol() { }); } +static void quit_cb(struct tray_menu *item) { + tray_exit(); +} + static void init() { mapstate.d2rProcess = new D2RProcess; HWND hwnd = (HWND)sapp_win32_get_hwnd(); ShowWindow(hwnd, SW_HIDE); DWORD style = WS_POPUP; - DWORD exStyle = WS_EX_LAYERED | WS_EX_TRANSPARENT; + DWORD exStyle = WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW; SetWindowLong(hwnd, GWL_STYLE, style); SetWindowLong(hwnd, GWL_EXSTYLE, exStyle); SetLayeredWindowAttributes(hwnd, 0, cfg->alpha, LWA_COLORKEY | LWA_ALPHA); + static tray_menu menu[] = { + {.text = (char*)"Quit", .cb = quit_cb}, + {.text = nullptr} + }; + + static tray tmenu = { + .icon = "D2RMH.exe", + .menu = menu, + }; + tray_init(&tmenu); + d2MapInit(cfg->d2Path.c_str()); initData(); initSokol(); @@ -762,6 +779,10 @@ static void checkForUpdate() { } static void frame() { + if (tray_loop(0) != 0) { + sapp_request_quit(); + return; + } checkForUpdate(); sg_begin_default_pass(&pass_action, sapp_width(), sapp_height()); if (mapstate.d2rProcess->available()) { @@ -861,9 +882,6 @@ sapp_desc sokol_main(int argc, char *argv[]) { .swap_interval = 1, .alpha = true, .window_title = "D2RMH", - .icon = { - .sokol_default = true, - }, .gl_force_gles2 = true, }; } diff --git a/src/res.rc b/src/res.rc new file mode 100644 index 0000000..a982007 --- /dev/null +++ b/src/res.rc @@ -0,0 +1 @@ +1 ICON "D2RMH.ico" diff --git a/src/tray/LICENSE b/src/tray/LICENSE new file mode 100644 index 0000000..b18604b --- /dev/null +++ b/src/tray/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Serge Zaitsev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/tray/README.md b/src/tray/README.md new file mode 100644 index 0000000..1ec6d23 --- /dev/null +++ b/src/tray/README.md @@ -0,0 +1,146 @@ +Tray +---- + +Cross-platform, single header, super tiny C99 implementation of a system tray icon with a popup menu. + +Works well on: + +* Linux/Gtk (libappindicator) +* Windows XP or newer (shellapi.h) +* MacOS (Cocoa/AppKit) + +There is also a stub implementation that returns errors on attempt to create a tray menu. + +# Setup + +Before you can compile `tray`, you'll need to add an environment definition before the line where you include `tray.h`. + +**For Windows:** +```c +#include +#include + +#define TRAY_WINAPI 1 + +#include "tray.h" +... +``` + +**For Linux:** +```c +#include +#include + +#define TRAY_APPINDICATOR 1 + +#include "tray.h" +... +``` + +**For Mac:** +```c +#include +#include + +#define TRAY_APPKIT 1 + +#include "tray.h" +... +``` + +# Demo + +The included example `.c` files can be compiled based on your environment. + +For example, to compile and run the program on Windows: + +```shell +$> gcc example_windows.c [Enter] +``` + +This will compile and build `a.out`. To run it: + +``` +$> a [Enter] +``` + +# Example + +```c +struct tray tray = { + .icon = "icon.png", + .menu = (struct tray_menu[]){{"Toggle me", 0, 0, toggle_cb, NULL}, + {"-", 0, 0, NULL, NULL}, + {"Quit", 0, 0, quit_cb, NULL}, + {NULL, 0, 0, NULL, NULL}}, +}; + +void toggle_cb(struct tray_menu *item) { + item->checked = !item->checked; + tray_update(&tray); +} + +void quit_cb(struct tray_menu *item) { + tray_exit(); +} + +... + +tray_init(&tray); +while (tray_loop(1) == 0); +tray_exit(); + +``` + +# API + +Tray structure defines an icon and a menu. +Menu is a NULL-terminated array of items. +Menu item defines menu text, menu checked and disabled (grayed) flags and a +callback with some optional context pointer. + +```c +struct tray { + char *icon; + struct tray_menu *menu; +}; + +struct tray_menu { + char *text; + int disabled; + int checked; + + void (*cb)(struct tray_menu *); + void *context; + + struct tray_menu *submenu; +}; +``` + +* `int tray_init(struct tray *)` - creates tray icon. Returns -1 if tray icon/menu can't be created. +* `void tray_update(struct tray *)` - updates tray icon and menu. +* `int tray_loop(int blocking)` - runs one iteration of the UI loop. Returns -1 if `tray_exit()` has been called. +* `void tray_exit()` - terminates UI loop. + +All functions are meant to be called from the UI thread only. + +Menu arrays must be terminated with a NULL item, e.g. the last item in the +array must have text field set to NULL. + +## Roadmap + +* [x] Cross-platform tray icon +* [x] Cross-platform tray popup menu +* [x] Separators in the menu +* [x] Disabled/enabled menu items +* [x] Checked/unchecked menu items +* [x] Nested menus +* [ ] Icons for menu items +* [x] Rewrite ObjC code in C using ObjC Runtime (now ObjC code breaks many linters and static analyzers) +* [ ] Call GTK code using dlopen/dlsym (to make binaries run safely if Gtk libraries are not available) + +## License + +This software is distributed under [MIT license](http://www.opensource.org/licenses/mit-license.php), + so feel free to integrate it in your commercial products. + diff --git a/src/tray/tray.h b/src/tray/tray.h new file mode 100644 index 0000000..5696951 --- /dev/null +++ b/src/tray/tray.h @@ -0,0 +1,370 @@ +#ifndef TRAY_H +#define TRAY_H + +struct tray_menu; + +struct tray { + const char *icon; + struct tray_menu *menu; +}; + +struct tray_menu { + char *text; + int disabled; + int checked; + + void (*cb)(struct tray_menu *); + void *context; + + struct tray_menu *submenu; +}; + +static void tray_update(struct tray *tray); + +#if defined(TRAY_APPINDICATOR) + +#include +#include + +#define TRAY_APPINDICATOR_ID "tray-id" + +static AppIndicator *indicator = NULL; +static int loop_result = 0; + +static void _tray_menu_cb(GtkMenuItem *item, gpointer data) { + (void)item; + struct tray_menu *m = (struct tray_menu *)data; + m->cb(m); +} + +static GtkMenuShell *_tray_menu(struct tray_menu *m) { + GtkMenuShell *menu = (GtkMenuShell *)gtk_menu_new(); + for (; m != NULL && m->text != NULL; m++) { + GtkWidget *item; + if (strcmp(m->text, "-") == 0) { + item = gtk_separator_menu_item_new(); + } else { + if (m->submenu != NULL) { + item = gtk_menu_item_new_with_label(m->text); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), + GTK_WIDGET(_tray_menu(m->submenu))); + } else { + item = gtk_check_menu_item_new_with_label(m->text); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), !!m->checked); + } + gtk_widget_set_sensitive(item, !m->disabled); + if (m->cb != NULL) { + g_signal_connect(item, "activate", G_CALLBACK(_tray_menu_cb), m); + } + } + gtk_widget_show(item); + gtk_menu_shell_append(menu, item); + } + return menu; +} + +static int tray_init(struct tray *tray) { + if (gtk_init_check(0, NULL) == FALSE) { + return -1; + } + indicator = app_indicator_new(TRAY_APPINDICATOR_ID, tray->icon, + APP_INDICATOR_CATEGORY_APPLICATION_STATUS); + app_indicator_set_status(indicator, APP_INDICATOR_STATUS_ACTIVE); + tray_update(tray); + return 0; +} + +static int tray_loop(int blocking) { + gtk_main_iteration_do(blocking); + return loop_result; +} + +static void tray_update(struct tray *tray) { + app_indicator_set_icon(indicator, tray->icon); + // GTK is all about reference counting, so previous menu should be destroyed + // here + app_indicator_set_menu(indicator, GTK_MENU(_tray_menu(tray->menu))); +} + +static void tray_exit() { loop_result = -1; } + +#elif defined(TRAY_APPKIT) + +#include +#include + +static id app; +static id pool; +static id statusBar; +static id statusItem; +static id statusBarButton; + +static id _tray_menu(struct tray_menu *m) { + id menu = objc_msgSend((id)objc_getClass("NSMenu"), sel_registerName("new")); + objc_msgSend(menu, sel_registerName("autorelease")); + objc_msgSend(menu, sel_registerName("setAutoenablesItems:"), false); + + for (; m != NULL && m->text != NULL; m++) { + if (strcmp(m->text, "-") == 0) { + objc_msgSend(menu, sel_registerName("addItem:"), + objc_msgSend((id)objc_getClass("NSMenuItem"), sel_registerName("separatorItem"))); + } else { + id menuItem = objc_msgSend((id)objc_getClass("NSMenuItem"), sel_registerName("alloc")); + objc_msgSend(menuItem, sel_registerName("autorelease")); + objc_msgSend(menuItem, sel_registerName("initWithTitle:action:keyEquivalent:"), + objc_msgSend((id)objc_getClass("NSString"), sel_registerName("stringWithUTF8String:"), m->text), + sel_registerName("menuCallback:"), + objc_msgSend((id)objc_getClass("NSString"), sel_registerName("stringWithUTF8String:"), "")); + + objc_msgSend(menuItem, sel_registerName("setEnabled:"), (m->disabled ? false : true)); + objc_msgSend(menuItem, sel_registerName("setState:"), (m->checked ? 1 : 0)); + objc_msgSend(menuItem, sel_registerName("setRepresentedObject:"), + objc_msgSend((id)objc_getClass("NSValue"), sel_registerName("valueWithPointer:"), m)); + + objc_msgSend(menu, sel_registerName("addItem:"), menuItem); + + if (m->submenu != NULL) { + objc_msgSend(menu, sel_registerName("setSubmenu:forItem:"), _tray_menu(m->submenu), menuItem); + } + } + } + + return menu; +} + +static void menu_callback(id self, SEL cmd, id sender) { + struct tray_menu *m = + (struct tray_menu *)objc_msgSend(objc_msgSend(sender, sel_registerName("representedObject")), + sel_registerName("pointerValue")); + + if (m != NULL && m->cb != NULL) { + m->cb(m); + } +} + +static int tray_init(struct tray *tray) { + pool = objc_msgSend((id)objc_getClass("NSAutoreleasePool"), + sel_registerName("new")); + + objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")); + + Class trayDelegateClass = objc_allocateClassPair(objc_getClass("NSObject"), "Tray", 0); + class_addProtocol(trayDelegateClass, objc_getProtocol("NSApplicationDelegate")); + class_addMethod(trayDelegateClass, sel_registerName("menuCallback:"), (IMP)menu_callback, "v@:@"); + objc_registerClassPair(trayDelegateClass); + + id trayDelegate = objc_msgSend((id)trayDelegateClass, + sel_registerName("new")); + + app = objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")); + + objc_msgSend(app, sel_registerName("setDelegate:"), trayDelegate); + + statusBar = objc_msgSend((id)objc_getClass("NSStatusBar"), + sel_registerName("systemStatusBar")); + + statusItem = objc_msgSend(statusBar, sel_registerName("statusItemWithLength:"), -1.0); + + objc_msgSend(statusItem, sel_registerName("retain")); + objc_msgSend(statusItem, sel_registerName("setHighlightMode:"), true); + statusBarButton = objc_msgSend(statusItem, sel_registerName("button")); + tray_update(tray); + objc_msgSend(app, sel_registerName("activateIgnoringOtherApps:"), true); + return 0; +} + +static int tray_loop(int blocking) { + id until = (blocking ? + objc_msgSend((id)objc_getClass("NSDate"), sel_registerName("distantFuture")) : + objc_msgSend((id)objc_getClass("NSDate"), sel_registerName("distantPast"))); + + id event = objc_msgSend(app, sel_registerName("nextEventMatchingMask:untilDate:inMode:dequeue:"), + ULONG_MAX, + until, + objc_msgSend((id)objc_getClass("NSString"), + sel_registerName("stringWithUTF8String:"), + "kCFRunLoopDefaultMode"), + true); + if (event) { + objc_msgSend(app, sel_registerName("sendEvent:"), event); + } + return 0; +} + +static void tray_update(struct tray *tray) { + objc_msgSend(statusBarButton, sel_registerName("setImage:"), + objc_msgSend((id)objc_getClass("NSImage"), sel_registerName("imageNamed:"), + objc_msgSend((id)objc_getClass("NSString"), sel_registerName("stringWithUTF8String:"), tray->icon))); + + objc_msgSend(statusItem, sel_registerName("setMenu:"), _tray_menu(tray->menu)); +} + +static void tray_exit() { objc_msgSend(app, sel_registerName("terminate:"), app); } + +#elif defined(TRAY_WINAPI) +#include + +#include + +#define WM_TRAY_CALLBACK_MESSAGE (WM_USER + 1) +#define WC_TRAY_CLASS_NAME "TRAY" +#define ID_TRAY_FIRST 1000 + +static WNDCLASSEX wc; +static NOTIFYICONDATA nid; +static HWND hwnd; +static HMENU hmenu = NULL; + +static LRESULT CALLBACK _tray_wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, + LPARAM lparam) { + switch (msg) { + case WM_CLOSE: + DestroyWindow(hwnd); + return 0; + case WM_DESTROY: + PostQuitMessage(0); + return 0; + case WM_TRAY_CALLBACK_MESSAGE: + if (lparam == WM_LBUTTONUP || lparam == WM_RBUTTONUP) { + POINT p; + GetCursorPos(&p); + SetForegroundWindow(hwnd); + WORD cmd = TrackPopupMenu(hmenu, TPM_LEFTALIGN | TPM_RIGHTBUTTON | + TPM_RETURNCMD | TPM_NONOTIFY, + p.x, p.y, 0, hwnd, NULL); + SendMessage(hwnd, WM_COMMAND, cmd, 0); + return 0; + } + break; + case WM_COMMAND: + if (wparam >= ID_TRAY_FIRST) { + MENUITEMINFO item = { + .cbSize = sizeof(MENUITEMINFO), .fMask = MIIM_ID | MIIM_DATA, + }; + if (GetMenuItemInfo(hmenu, wparam, FALSE, &item)) { + struct tray_menu *menu = (struct tray_menu *)item.dwItemData; + if (menu != NULL && menu->cb != NULL) { + menu->cb(menu); + } + } + return 0; + } + break; + } + return DefWindowProc(hwnd, msg, wparam, lparam); +} + +static HMENU _tray_menu(struct tray_menu *m, UINT *id) { + HMENU hmenu = CreatePopupMenu(); + for (; m != NULL && m->text != NULL; m++, (*id)++) { + if (strcmp(m->text, "-") == 0) { + InsertMenu(hmenu, *id, MF_SEPARATOR, TRUE, ""); + } else { + MENUITEMINFO item; + memset(&item, 0, sizeof(item)); + item.cbSize = sizeof(MENUITEMINFO); + item.fMask = MIIM_ID | MIIM_TYPE | MIIM_STATE | MIIM_DATA; + item.fType = 0; + item.fState = 0; + if (m->submenu != NULL) { + item.fMask = item.fMask | MIIM_SUBMENU; + item.hSubMenu = _tray_menu(m->submenu, id); + } + if (m->disabled) { + item.fState |= MFS_DISABLED; + } + if (m->checked) { + item.fState |= MFS_CHECKED; + } + item.wID = *id; + item.dwTypeData = m->text; + item.dwItemData = (ULONG_PTR)m; + + InsertMenuItem(hmenu, *id, TRUE, &item); + } + } + return hmenu; +} + +static int tray_init(struct tray *tray) { + memset(&wc, 0, sizeof(wc)); + wc.cbSize = sizeof(WNDCLASSEX); + wc.lpfnWndProc = _tray_wnd_proc; + wc.hInstance = GetModuleHandle(NULL); + wc.lpszClassName = WC_TRAY_CLASS_NAME; + if (!RegisterClassEx(&wc)) { + return -1; + } + + hwnd = CreateWindowEx(0, WC_TRAY_CLASS_NAME, NULL, 0, 0, 0, 0, 0, 0, 0, 0, 0); + if (hwnd == NULL) { + return -1; + } + UpdateWindow(hwnd); + + memset(&nid, 0, sizeof(nid)); + nid.cbSize = sizeof(NOTIFYICONDATA); + nid.hWnd = hwnd; + nid.uID = 0; + nid.uFlags = NIF_ICON | NIF_MESSAGE; + nid.uCallbackMessage = WM_TRAY_CALLBACK_MESSAGE; + Shell_NotifyIcon(NIM_ADD, &nid); + + tray_update(tray); + return 0; +} + +static int tray_loop(int blocking) { + MSG msg; + if (blocking) { + GetMessage(&msg, NULL, 0, 0); + } else { + PeekMessage(&msg, NULL, 0, 0, PM_REMOVE); + } + if (msg.message == WM_QUIT) { + return -1; + } + TranslateMessage(&msg); + DispatchMessage(&msg); + return 0; +} + +static void tray_update(struct tray *tray) { + HMENU prevmenu = hmenu; + UINT id = ID_TRAY_FIRST; + hmenu = _tray_menu(tray->menu, &id); + SendMessage(hwnd, WM_INITMENUPOPUP, (WPARAM)hmenu, 0); + HICON icon; + ExtractIconEx(tray->icon, 0, NULL, &icon, 1); + if (nid.hIcon) { + DestroyIcon(nid.hIcon); + } + nid.hIcon = icon; + Shell_NotifyIcon(NIM_MODIFY, &nid); + + if (prevmenu != NULL) { + DestroyMenu(prevmenu); + } +} + +static void tray_exit() { + Shell_NotifyIcon(NIM_DELETE, &nid); + if (nid.hIcon != 0) { + DestroyIcon(nid.hIcon); + } + if (hmenu != 0) { + DestroyMenu(hmenu); + } + PostQuitMessage(0); + UnregisterClass(WC_TRAY_CLASS_NAME, GetModuleHandle(NULL)); +} +#else +static int tray_init(struct tray *tray) { return -1; } +static int tray_loop(int blocking) { return -1; } +static void tray_update(struct tray *tray) {} +static void tray_exit(); +#endif + +#endif /* TRAY_H */