Skip to content
Open
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
4 changes: 3 additions & 1 deletion application.go
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,8 @@ func AppendToWalkInit(fn func()) {
}

func appWinEventProc(hook win.HWINEVENTHOOK, event uint32, hwnd win.HWND, idObject int32, idChild int32, idEventThread uint32, eventTimeMilliseconds uint32) uintptr {
defer appSingleton.HandlePanicFromNativeCallback()

switch event {
case win.EVENT_OBJECT_CLOAKED, win.EVENT_OBJECT_UNCLOAKED:
var wparam uintptr
Expand Down Expand Up @@ -868,7 +870,7 @@ func (app *Application) DeletePreTranslateHandlerForHWND(hwnd win.HWND) {
// Go may be called from any goroutine. Go will not run f if
// [(*Application).Exit] has already been called.
func (app *Application) Go(f func(context.Context)) {
if app.ctx.Err() != nil {
if f == nil || app.ctx.Err() != nil {
return
}

Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
module github.com/tailscale/walk

go 1.21
go 1.24.0

require (
github.com/dblohm7/wingoes v0.0.0-20231019175336-f6e33aa7cc34
github.com/tailscale/win v0.0.0-20250213223159-5992cb43ca35
github.com/tailscale/win v0.0.0-20260619195133-2d76c33a64c1
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53
golang.org/x/sys v0.8.0
golang.org/x/sys v0.37.0
gopkg.in/Knetic/govaluate.v3 v3.0.0
)
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
github.com/dblohm7/wingoes v0.0.0-20231019175336-f6e33aa7cc34 h1:FBMro26TLQwBk+n4fbTSmSf3QUKb09pvW4fz49lxpl0=
github.com/dblohm7/wingoes v0.0.0-20231019175336-f6e33aa7cc34/go.mod h1:6NCrWM5jRefaG7iN0iMShPalLsljHWBh9v1zxM2f8Xs=
github.com/tailscale/win v0.0.0-20250213223159-5992cb43ca35 h1:wAZbkTZkqDzWsqxPh2qkBd3KvFU7tcxV0BP0Rnhkxog=
github.com/tailscale/win v0.0.0-20250213223159-5992cb43ca35/go.mod h1:aMd4yDHLjbOuYP6fMxj1d9ACDQlSWwYztcpybGHCQc8=
github.com/tailscale/win v0.0.0-20260619195133-2d76c33a64c1 h1:QzOW7bZOlg5/hi1W6dStSN9qazTHkHj5kO7sTA5aymY=
github.com/tailscale/win v0.0.0-20260619195133-2d76c33a64c1/go.mod h1:vwG7VvIPzIYNJd0Nio6PZW7yr8KNNFEc8EVQ+J/B4bk=
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o=
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc=
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
88 changes: 75 additions & 13 deletions notifyicon.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,23 @@ func (niw *notifyIconWindow) Dispose() {
niw.WindowBase.Dispose()
}

func (niw *notifyIconWindow) nidToNotifyIcon(nid uint16) *NotifyIcon {
if ni := niw.owner; ni != nil {
return ni
}

// No GUID, try resolving via integral ID.
return notifyIconIDs[nid]
}

func (niw *notifyIconWindow) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr {
switch msg {
case notifyIconMessageID:
lp32 := uint32(lParam)
ni := niw.owner
if ni == nil {
// No GUID, try resolving via integral ID.
ni = notifyIconIDs[win.HIWORD(lp32)]
if ni == nil {
// We don't need to call DefWindowProc because this is an app-defined message.
return 0
}
if ni := niw.nidToNotifyIcon(win.HIWORD(lp32)); ni != nil {
ni.wndProc(hwnd, win.LOWORD(lp32), wParam)
}

ni.wndProc(hwnd, win.LOWORD(lp32), wParam)
// We don't need to call DefWindowProc because this is an app-defined message.
return 0
case taskbarCreatedMsgId:
Expand All @@ -73,6 +75,12 @@ func (niw *notifyIconWindow) WndProc(hwnd win.HWND, msg uint32, wParam, lParam u
niw.forIcon(func(ni *NotifyIcon) { ni.activeContextMenus++ })
case win.WM_EXITMENULOOP:
niw.forIcon(func(ni *NotifyIcon) { ni.activeContextMenus-- })
case win.WM_TIMER:
tid := niTimerID(wParam)
if ni := niw.nidToNotifyIcon(tid.notificationIconID()); ni != nil {
ni.wndProc(hwnd, win.WM_TIMER, uintptr(tid.notificationCode()))
return 0
}
default:
}

Expand Down Expand Up @@ -100,9 +108,28 @@ func (ni *NotifyIcon) wndProc(hwnd win.HWND, msg uint16, wParam uintptr) {
case win.WM_LBUTTONDOWN:
ni.mouseDownPublisher.Publish(int(win.GET_X_LPARAM(wParam)), int(win.GET_Y_LPARAM(wParam)), LeftButton)

// We treat keyboard selection of the icon identically to a left-click.
// All three messages use the same format for wParam.
case win.NIN_KEYSELECT, win.NIN_SELECT, win.WM_LBUTTONUP:
case win.NIN_KEYSELECT:
// NIN_KEYSELECT is extremely poorly documented, but Spy++ dumps show it
// delivering a second notification immediately after the first under certain
// conditions (such as when the enter key is used to make the selection).
// Since we're only interested in the first notification, we set a timer
// and ignore any further notifications for a short duration of time.
// The double-click time feels like a sufficient duration to suppress
// the extras.
if ni.keySelectTimerID != 0 {
return
}

// We use a HWND-based timer since we're already processing messages for
// the current window: timer notifications will therefore be posted via
// the same message queue already being used for keyboard input.
// We include ni's identifier in the timer ID to ensure that we can route
// the WM_TIMER to the appropriate NotifyIcon.
tid := makeNotifyIconTimerID(win.NIN_KEYSELECT, ni.id())
ni.keySelectTimerID = niTimerID(win.SetTimer(hwnd, uintptr(tid), win.GetDoubleClickTime(), 0))
fallthrough

case win.NIN_SELECT:
if ni.activeContextMenus > 0 {
win.PostMessage(hwnd, win.WM_CANCELMODE, 0, 0)
break
Expand Down Expand Up @@ -142,6 +169,13 @@ func (ni *NotifyIcon) wndProc(hwnd win.HWND, msg uint16, wParam uintptr) {
case win.NIN_BALLOONUSERCLICK:
ni.reEnableToolTip()
ni.messageClickedPublisher.Publish()

case win.WM_TIMER:
if wParam == win.NIN_KEYSELECT {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: IIUC, this code only runs when ni.wndProc is explicitly invoked on line 81, and we replace the original timer ID with uintptr(tid.notificationCode()) at the call site. So I think this should work fine.

At the same time, as written, it looks like a normal message handler, so comparing wParam (which, again, should be the timer identifier for a normal WM_TIMER message) to win.NIN_KEYSELECT is confusing.

I think we should structure this differently and perhaps plumb the timer ID all the way here, so we can compare it against ni.keySelectTimerID.

But if I'm missing something and that's not possible (or not a good idea), could you please document that this is a fake message? Specifically, IIUC, the handler will not be invoked for any real WM_TIMER messages, and the only expected wParam value is win.NIN_KEYSELECT, so the check is fine.

if win.KillTimer(hwnd, uintptr(ni.keySelectTimerID)) {
ni.keySelectTimerID = 0
}
}
}
}

Expand Down Expand Up @@ -232,7 +266,7 @@ func newNotificationIconWindow() (*notifyIconWindow, error) {
ClassName: notifyIconWindowClass,
// Creating the window with WS_DISABLED in an effort to dissuade screen
// readers from treating the hidden window as focusable content.
Style: win.WS_OVERLAPPEDWINDOW | win.WS_DISABLED,
Style: win.WS_OVERLAPPEDWINDOW | win.WS_DISABLED,
// Always create the window at the origin, thus ensuring that the window
// resides on the desktop's primary monitor, which is the same monitor where
// the taskbar notification area resides. This ensures that the window's
Expand Down Expand Up @@ -509,6 +543,20 @@ func (cmd *niCmd) execute() error {
return showTipCmd.execute()
}

type niTimerID uintptr

func (tid niTimerID) notificationCode() uint16 {
return uint16(tid)
}

func (tid niTimerID) notificationIconID() uint16 {
return win.HIWORD(uint32(tid))
}

func makeNotifyIconTimerID(notificationCode, notifyIconID uint16) niTimerID {
return niTimerID(win.MAKELONG(notificationCode, notifyIconID))
}

// NotifyIcon represents an icon in the taskbar notification area.
type NotifyIcon struct {
shellIcon *shellNotificationIcon
Expand All @@ -520,6 +568,7 @@ type NotifyIcon struct {
messageClickedPublisher EventPublisher
showingContextMenuPublisher ProceedEventPublisher
activeContextMenus int // int because Win32 permits nested context menus
keySelectTimerID niTimerID
disableShowContextMenu bool
visible bool
}
Expand Down Expand Up @@ -571,6 +620,19 @@ func newNotifyIcon(guid *windows.GUID) (*NotifyIcon, error) {
return ni, nil
}

func (ni *NotifyIcon) id() uint16 {
si := ni.shellIcon
if si == nil {
return 0
}

if pid := si.id; pid != nil {
return uint16(*pid)
}

return 0
}

func (ni *NotifyIcon) DPI() int {
return ni.shellIcon.window.DPI()
}
Expand Down
Loading