From 22ba5970dfdd367800b2e6451ed31c1925e7ccb9 Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Mon, 10 Jan 2022 01:24:10 -0600 Subject: [PATCH] windows system tray draft --- examples/tray.nim | 44 ++++++++ src/windy/platforms/win32/platform.nim | 145 ++++++++++++++++++++++++- src/windy/platforms/win32/utils.nim | 6 +- src/windy/platforms/win32/windefs.nim | 83 +++++++++++++- windy.nimble | 1 + 5 files changed, 274 insertions(+), 5 deletions(-) create mode 100644 examples/tray.nim diff --git a/examples/tray.nim b/examples/tray.nim new file mode 100644 index 0000000..2700161 --- /dev/null +++ b/examples/tray.nim @@ -0,0 +1,44 @@ +import pixie, windy + +when defined(windows): + # Tray API only currently supported on Windows + + let window = newWindow("Windy Tray Icon", ivec2(1280, 800)) + window.makeContextCurrent() + + let + icon = newImage(64, 64) + path = newPath() + path.circle(circle(vec2(32, 32), 26)) + icon.fillPath(path, color(0.3, 0.6, 0.9, 1)) + + proc onTrayIconClick() = + echo "Tray icon clicked" + + var menu: seq[TrayMenuEntry] + menu.add(TrayMenuEntry( + kind: TrayMenuOption, + text: "Option 1", + onClick: proc() = + echo "Option 1 clicked" + )) + menu.add(TrayMenuEntry( + kind: TrayMenuOption, + text: "Option 2", + onClick: proc() = + echo "Option 2 clicked" + )) + menu.add(TrayMenuEntry(kind: TrayMenuSeparator)) + menu.add(TrayMenuEntry( + kind: TrayMenuOption, + text: "Quit Demo", + onClick: proc() = + window.closeRequested = true + )) + + showTrayIcon(icon, "Demo", onTrayIconClick, menu) + + while not window.closeRequested: + pollEvents() + + hideTrayIcon() diff --git a/src/windy/platforms/win32/platform.nim b/src/windy/platforms/win32/platform.nim index 0decda6..82ca8ca 100644 --- a/src/windy/platforms/win32/platform.nim +++ b/src/windy/platforms/win32/platform.nim @@ -1,7 +1,9 @@ -import ../../common, ../../internal, times, unicode, utils, vmath, windefs +import ../../common, ../../internal, pixie/images, pixie/fileformats/png, times, + unicode, utils, vmath, windefs, flatty/hashy const windowClassName = "WINDY0" + trayIconId = 2022 defaultScreenDpi = 96 wheelDelta = 120 decoratedWindowStyle = WS_OVERLAPPEDWINDOW @@ -29,6 +31,8 @@ const # WGL_CONTEXT_DEBUG_BIT_ARB = 0x0001 WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB = 0x0002 + WM_TRAY_ICON = WM_APP + 0 + type Window* = ref object onCloseRequest*: Callback @@ -56,6 +60,17 @@ type style: LONG rect: RECT + TrayMenyEntryKind* = enum + TrayMenuOption, TrayMenuSeparator + + TrayMenuEntry* = object + case kind*: TrayMenyEntryKind + of TrayMenuOption: + text*: string + onClick*: Callback + of TrayMenuSeparator: + discard + var wglCreateContext: wglCreateContext wglDeleteContext: wglDeleteContext @@ -73,6 +88,10 @@ var var helperWindow: HWND windows: seq[Window] + iconCache: seq[(Hash, HICON)] + onTrayIconClick: Callback + trayMenuHandle: HMENU + trayMenuEntries: seq[TrayMenuEntry] proc indexForHandle(windows: seq[Window], hWnd: HWND): int = ## Returns the window for this handle, else -1 @@ -528,9 +547,39 @@ proc createHelperWindow(): HWND = let helperWindowClassName = "WindyHelper" proc helperWndProc( - hWnd: HWND, uMsg: UINT, wParam: WPARAM, lParam: LPARAM + hWnd: HWND, + uMsg: UINT, + wParam: WPARAM, + lParam: LPARAM ): LRESULT {.stdcall.} = - DefWindowProcW(hWnd, uMsg, wParam, lParam) + case uMsg: + of WM_APP: + let innerMsg = LOWORD(lParam) + case innerMsg: + of WM_LBUTTONUP: + if onTrayIconClick != nil: + onTrayIconClick() + of WM_RBUTTONUP: + if trayMenuHandle > 0: + var pos: POINT + discard GetCursorPos(pos.addr) + let clicked = TrackPopupMenu( + trayMenuHandle, + TPM_RETURNCMD, + pos.x, + pos.y, + 0, + helperWindow, + nil + ).int + if clicked > 0: + if trayMenuEntries[clicked - 1].onClick != nil: + trayMenuEntries[clicked - 1].onClick() + else: + discard + return 0 + else: + DefWindowProcW(hWnd, uMsg, wParam, lParam) registerWindowClass(helperWindowClassName, helperWndProc) @@ -998,3 +1047,93 @@ proc setClipboardString*(value: string) = discard EmptyClipboard() discard SetClipboardData(CF_UNICODETEXT, dataHandle) discard CloseClipboard() + +proc createIconHandle(image: Image): HICON = + let iconHash = hashy(image.data[0].addr, image.data.len * 4) + + for (hash, handle) in iconCache: + if iconHash == hash: + result = handle + break + + if result == 0: + let encoded = image.encodePng() + result = CreateIconFromResourceEx( + cast[PBYTE](encoded[0].unsafeAddr), + encoded.len.DWORD, + TRUE, + 0x00030000, + 0, + 0, + 0 + ) + + if result != 0: + iconCache.add((iconHash, result)) + else: + raise newException(WindyError, "Error creating tray icon") + +proc showTrayIcon*( + icon: Image, + tooltip: string, + onClick: Callback, + menu: seq[TrayMenuEntry] = @[] +) = + if trayMenuHandle != 0: + discard DestroyMenu(trayMenuHandle) + trayMenuHandle = 0 + trayMenuEntries = @[] + + if menu.len > 0: + trayMenuEntries = menu + trayMenuHandle = CreatePopupMenu() + for i, entry in menu: + case entry.kind: + of TrayMenuOption: + let wstr = entry.text.wstr() + discard AppendMenuW( + trayMenuHandle, + MF_STRING, + (i + 1).UINT_PTR, + cast[ptr WCHAR](wstr[0].unsafeAddr) + ) + of TrayMenuSeparator: + discard AppendMenuW(trayMenuHandle, MF_SEPARATOR, 0, nil) + + onTrayIconClick = onClick + + var nid: NOTIFYICONDATAW + nid.cbSize = sizeof(NOTIFYICONDATAW).DWORD + nid.hWnd = helperWindow + nid.uID = trayIconId + nid.uFlags = NIF_MESSAGE or NIF_ICON + nid.uCallbackMessage = WM_TRAY_ICON + nid.hIcon = icon.createIconHandle() + nid.union1.uVersion = NOTIFYICON_VERSION_4 + + if tooltip != "": + nid.uFlags = nid.uFlags or NIF_TIP or NIF_SHOWTIP + + let wstr = tooltip.wstr() + copyMem( + nid.szTip[0].addr, + wstr[0].unsafeAddr, + min(nid.szTip.high, wstr.high) * 2 # Leave room for null terminator + ) + + discard Shell_NotifyIconW(NIM_ADD, nid.addr) + +proc hideTrayIcon*() = + var nid: NOTIFYICONDATAW + nid.cbSize = sizeof(NOTIFYICONDATAW).DWORD + nid.hWnd = helperWindow + nid.uID = trayIconId + + discard Shell_NotifyIconW(NIM_DELETE, nid.addr) + + onTrayIconClick = nil + + if trayMenuHandle != 0: + discard DestroyMenu(trayMenuHandle) + trayMenuHandle = 0 + trayMenuEntries = @[] diff --git a/src/windy/platforms/win32/utils.nim b/src/windy/platforms/win32/utils.nim index 06e1beb..86e3847 100644 --- a/src/windy/platforms/win32/utils.nim +++ b/src/windy/platforms/win32/utils.nim @@ -54,7 +54,7 @@ template HIWORD*(param: WPARAM | LPARAM): int16 = cast[int16]((param shr 16)) template LOWORD*(param: WPARAM | LPARAM): int16 = - cast[int16](param and uint16.high) + cast[int16](param and uint16.high.WPARAM) const scancodeToButton* = block: var s = newSeq[Button](512) @@ -279,5 +279,9 @@ proc wmEventName*(wm: int | UINT): string = "WM_IME_ENDCOMPOSITION" of WM_IME_COMPOSITION: "WM_IME_COMPOSITION" + of WM_USER: + "WM_USER" + of WM_APP: + "WM_APP" else: "WM " & $wm & " " & $toHex(wm) diff --git a/src/windy/platforms/win32/windefs.nim b/src/windy/platforms/win32/windefs.nim index 1da65f3..95df633 100644 --- a/src/windy/platforms/win32/windefs.nim +++ b/src/windy/platforms/win32/windefs.nim @@ -9,6 +9,7 @@ else: type BYTE* = uint8 + PBYTE* = ptr BYTE SHORT* = int16 BOOL* = int32 LPBOOL* = ptr BOOL @@ -44,7 +45,7 @@ type WPARAM* = UINT_PTR LPARAM* = LONG_PTR LRESULT* = LONG_PTR - WNDPROC* = proc ( + WNDPROC* = proc( hWnd: HWND, uMsg: UINT, wParam: WPARAM, @@ -141,6 +142,31 @@ type ptCurrentPos*: POINT rcArea*: RECT LPCANDIDATEFORM* = ptr CANDIDATEFORM + GUID* {.pure.} = object + Data1*: int32 + Data2*: uint16 + Data3*: uint16 + Data4*: array[8, uint8] + NOTIFYICONDATAW_UNION1* {.pure, union.} = object + uTimeout*: UINT + uVersion*: UINT + NOTIFYICONDATAW* {.pure.} = object + cbSize*: DWORD + hWnd*: HWND + uID*: UINT + uFlags*: UINT + uCallbackMessage*: UINT + hIcon*: HICON + szTip*: array[128, WCHAR] + dwState*: DWORD + dwStateMask*: DWORD + szInfo*: array[256, WCHAR] + union1*: NOTIFYICONDATAW_UNION1 + szInfoTitle*: array[64, WCHAR] + dwInfoFlags*: DWORD + guidItem*: GUID + hBalloonIcon*: HICON + PNOTIFYICONDATAW* = ptr NOTIFYICONDATAW type wglCreateContext* = proc(hdc: HDC): HGLRC {.stdcall, raises: [].} @@ -288,6 +314,8 @@ const WM_DWMWINDOWMAXIMIZEDCHANGE* = 0x0321 WM_DWMSENDICONICTHUMBNAIL* = 0x0323 WM_DWMSENDICONICLIVEPREVIEWBITMAP* = 0x0326 + WM_USER* = 0x0400 + WM_APP* = 0x8000 SC_RESTORE* = 0xF120 SC_MINIMIZE* = 0xF020 SC_MAXIMIZE* = 0xF030 @@ -388,6 +416,23 @@ const IACE_CHILDREN* = 0x0001 IACE_DEFAULT* = 0x0010 IACE_IGNORENOCONTEXT* = 0x0020 + NOTIFYICON_VERSION_4* = 4 + NIM_ADD* = 0x00000000 + NIM_MODIFY* = 0x00000001 + NIM_DELETE* = 0x00000002 + NIM_SETFOCUS* = 0x00000003 + NIM_SETVERSION* = 0x00000004 + NIF_MESSAGE* = 0x00000001 + NIF_ICON* = 0x00000002 + NIF_TIP* = 0x00000004 + NIF_STATE* = 0x00000008 + NIF_INFO* = 0x00000010 + NIF_GUID* = 0x00000020 + NIF_REALTIME* = 0x00000040 + NIF_SHOWTIP* = 0x00000080 + MF_STRING* = 0x00000000 + MF_SEPARATOR* = 0x00000800 + TPM_RETURNCMD* = 0x0100 {.push importc, stdcall.} @@ -679,6 +724,37 @@ proc SetCaretPos*( y: int32 ): BOOL {.dynlib: "User32".} +proc CreateIconFromResourceEx*( + presbits: PBYTE, + dwResSize: DWORD, + fIcon: BOOL, + dwVer: DWORD, + cxDesired: int32, + cyDesired: int32, + Flags: UINT +): HICON {.dynlib: "User32".} + +proc CreatePopupMenu*(): HMENU {.dynlib: "User32".} + +proc DestroyMenu*(hMenu: HMENU): BOOL {.dynlib: "User32".} + +proc AppendMenuW*( + hMenu: HMENU, + uFlags: UINT, + uIDNewItem: UINT_PTR, + lpNewItem: LPCWSTR +): BOOL {.dynlib: "User32".} + +proc TrackPopupMenu*( + hMenu: HMENU, + uFlags: UINT, + x: int32, + y: int32, + nReserved: int32, + hWnd: HWND, + prcRect: ptr RECT +): BOOL {.dynlib: "User32".} + proc ChoosePixelFormat*( hdc: HDC, ppfd: ptr PIXELFORMATDESCRIPTOR @@ -740,4 +816,9 @@ proc ImmAssociateContextEx*( dwFlags: DWORD ): BOOL {.dynlib: "imm32".} +proc Shell_NotifyIconW*( + dwMessage: DWORD, + lpData: PNOTIFYICONDATAW +): BOOL {.dynlib: "shell32".} + {.pop.} diff --git a/windy.nimble b/windy.nimble index f67f314..d234bed 100644 --- a/windy.nimble +++ b/windy.nimble @@ -8,3 +8,4 @@ srcDir = "src" requires "nim >= 1.4.8" requires "vmath >= 1.1.0" requires "opengl >= 1.2.6" +requires "pixie >= 3.1.2"