Skip to content

Commit 48547f3

Browse files
committed
notifyicon.go: a11y improvements to notification icons
We handle NIN_KEYSELECT and NIN_SELECT identically to WM_LBUTTONUP. We initialize the hidden event sink window with WS_DISABLED and annotate it with a window role instead of a client role, in the hope that this will discourage screen readers from focusing on it. Finally, we do a bit of cleanup since we're already in here. Updates tailscale/corp#29972 Signed-off-by: Aaron Klotz <aaron@tailscale.com>
1 parent b2c15a4 commit 48547f3

1 file changed

Lines changed: 57 additions & 21 deletions

File tree

notifyicon.go

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,9 @@ func (ni *NotifyIcon) wndProc(hwnd win.HWND, msg uint16, wParam uintptr) {
100100
case win.WM_LBUTTONDOWN:
101101
ni.mouseDownPublisher.Publish(int(win.GET_X_LPARAM(wParam)), int(win.GET_Y_LPARAM(wParam)), LeftButton)
102102

103-
case win.WM_LBUTTONUP:
103+
// We treat keyboard selection of the icon identically to a left-click.
104+
// All three messages use the same format for wParam.
105+
case win.NIN_KEYSELECT, win.NIN_SELECT, win.WM_LBUTTONUP:
104106
if ni.activeContextMenus > 0 {
105107
win.PostMessage(hwnd, win.WM_CANCELMODE, 0, 0)
106108
break
@@ -197,8 +199,9 @@ func (ni *NotifyIcon) doContextMenu(hwnd win.HWND, x, y int32) {
197199
}
198200

199201
func isTaskbarPresent() bool {
200-
var abd win.APPBARDATA
201-
abd.CbSize = uint32(unsafe.Sizeof(abd))
202+
abd := win.APPBARDATA{
203+
CbSize: uint32(unsafe.Sizeof(win.APPBARDATA{})),
204+
}
202205
return win.SHAppBarMessage(win.ABM_GETTASKBARPOS, &abd) != 0
203206
}
204207

@@ -227,7 +230,7 @@ func newNotificationIconWindow() (*notifyIconWindow, error) {
227230
niwCfg := windowCfg{
228231
Window: niw,
229232
ClassName: notifyIconWindowClass,
230-
Style: win.WS_OVERLAPPEDWINDOW,
233+
Style: win.WS_OVERLAPPEDWINDOW | win.WS_DISABLED,
231234
// Always create the window at the origin, thus ensuring that the window
232235
// resides on the desktop's primary monitor, which is the same monitor where
233236
// the taskbar notification area resides. This ensures that the window's
@@ -239,6 +242,10 @@ func newNotificationIconWindow() (*notifyIconWindow, error) {
239242
if err := initWindowWithCfg(&niwCfg); err != nil {
240243
return nil, err
241244
}
245+
246+
// By default the window has the "client" role, which suggests content.
247+
// Assigning the "window" role instead.
248+
niw.Accessibility().SetRole(AccRoleWindow)
242249
return niw, nil
243250
}
244251

@@ -275,19 +282,10 @@ func newShellNotificationIcon(guid *windows.GUID) (*shellNotificationIcon, error
275282
return shellIcon, nil
276283
}
277284

278-
if guid != nil {
279-
// If we're using a GUID, an add operation can fail if a previous instance
280-
// using this GUID terminated abnormally and its notification icon was left
281-
// behind on the taskbar. Preemptively delete any pre-existing icon.
282-
if delCmd := shellIcon.newCmd(win.NIM_DELETE); delCmd != nil {
283-
// The previous instance would have used a different, now-defunct HWND, so
284-
// we can't use one here...
285-
delCmd.nid.HWnd = win.HWND(0)
286-
// We expect delCmd.execute() to fail if there isn't a pre-existing icon,
287-
// so no error checking for this call.
288-
delCmd.execute()
289-
}
290-
}
285+
// If we're using a GUID, an add operation can fail if a previous instance
286+
// using this GUID terminated abnormally and its notification icon was left
287+
// behind on the taskbar. Preemptively delete any pre-existing icon.
288+
shellIcon.clearAnyPreExisting()
291289

292290
// Add our notify icon to the status area and make sure it is hidden.
293291
addCmd := shellIcon.newCmd(win.NIM_ADD)
@@ -300,13 +298,32 @@ func newShellNotificationIcon(guid *windows.GUID) (*shellNotificationIcon, error
300298
return shellIcon, nil
301299
}
302300

301+
// clearAnyPreExisting deletes any GUID-based notification icon that might
302+
// still exist after either the shell restarts or this app restarts. Either
303+
// way, re-adding an icon with the same GUID will fail unless we delete the
304+
// previous instance first.
305+
func (i *shellNotificationIcon) clearAnyPreExisting() {
306+
// Only meaningful for GUID-based icons.
307+
if i.guid == nil {
308+
return
309+
}
310+
311+
if delCmd := i.newCmd(win.NIM_DELETE); delCmd != nil {
312+
// The previous instance would have used a different, now-defunct HWND, so
313+
// we can't use one here...
314+
delCmd.nid.HWnd = win.HWND(0)
315+
// We expect delCmd.execute() to fail if there isn't a pre-existing icon,
316+
// so no error checking for this call.
317+
delCmd.execute()
318+
}
319+
}
320+
303321
func (i *shellNotificationIcon) setOwner(ni *NotifyIcon) {
304322
// Only icons identified via GUID use the owner field; non-GUID icons share
305323
// the same window and thus need to be looked up via notifyIconIDs.
306-
if i.guid == nil {
307-
return
324+
if i.guid != nil {
325+
i.window.owner = ni
308326
}
309-
i.window.owner = ni
310327
}
311328

312329
func (i *shellNotificationIcon) Dispose() error {
@@ -455,6 +472,13 @@ func (cmd *niCmd) setVisible(v bool) {
455472
}
456473

457474
func (cmd *niCmd) execute() error {
475+
var addShowTip bool
476+
if cmd.op == win.NIM_ADD && (cmd.nid.UFlags&win.NIF_SHOWTIP) != 0 {
477+
// NIF_SHOWTIP is a v4 flag. Don't include it in flags for NIM_ADD, which
478+
// is a v1 operation. We add it back in below, after we've upgraded to v4.
479+
addShowTip = true
480+
cmd.nid.UFlags ^= win.NIF_SHOWTIP
481+
}
458482
if !win.Shell_NotifyIcon(cmd.op, &cmd.nid) {
459483
return lastError(fmt.Sprintf("Shell_NotifyIcon(%d, %#v)", cmd.op, cmd.nid))
460484
}
@@ -473,7 +497,14 @@ func (cmd *niCmd) execute() error {
473497
verCmd.op = win.NIM_SETVERSION
474498
// Use Vista+ behaviour.
475499
verCmd.nid.UVersion = win.NOTIFYICON_VERSION_4
476-
return verCmd.execute()
500+
if err := verCmd.execute(); err != nil || !addShowTip {
501+
return err
502+
}
503+
504+
showTipCmd := *cmd
505+
showTipCmd.op = win.NIM_MODIFY
506+
showTipCmd.nid.UFlags |= win.NIF_SHOWTIP
507+
return showTipCmd.execute()
477508
}
478509

479510
// NotifyIcon represents an icon in the taskbar notification area.
@@ -551,6 +582,11 @@ func (ni *NotifyIcon) reAddToTaskbar() {
551582
// track this once the add command successfully executes.
552583
prevID := ni.shellIcon.id
553584

585+
// If we're using a GUID, an add operation can fail if a previous instance
586+
// using this GUID terminated abnormally and its notification icon was left
587+
// behind on the taskbar. Preemptively delete any pre-existing icon.
588+
ni.shellIcon.clearAnyPreExisting()
589+
554590
cmd := ni.shellIcon.newCmd(win.NIM_ADD)
555591
cmd.setCallbackMessage(notifyIconMessageID)
556592
cmd.setVisible(ni.visible)

0 commit comments

Comments
 (0)