Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

windows system tray draft #44

Merged
merged 1 commit into from
Jan 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions examples/tray.nim
Original file line number Diff line number Diff line change
@@ -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()
145 changes: 142 additions & 3 deletions src/windy/platforms/win32/platform.nim
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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 = @[]
6 changes: 5 additions & 1 deletion src/windy/platforms/win32/utils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
83 changes: 82 additions & 1 deletion src/windy/platforms/win32/windefs.nim
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ else:

type
BYTE* = uint8
PBYTE* = ptr BYTE
SHORT* = int16
BOOL* = int32
LPBOOL* = ptr BOOL
Expand Down Expand Up @@ -44,7 +45,7 @@ type
WPARAM* = UINT_PTR
LPARAM* = LONG_PTR
LRESULT* = LONG_PTR
WNDPROC* = proc (
WNDPROC* = proc(
hWnd: HWND,
uMsg: UINT,
wParam: WPARAM,
Expand Down Expand Up @@ -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: [].}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -740,4 +816,9 @@ proc ImmAssociateContextEx*(
dwFlags: DWORD
): BOOL {.dynlib: "imm32".}

proc Shell_NotifyIconW*(
dwMessage: DWORD,
lpData: PNOTIFYICONDATAW
): BOOL {.dynlib: "shell32".}

{.pop.}
1 change: 1 addition & 0 deletions windy.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -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"