Skip to content

Commit 6376def

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 6376def

File tree

1 file changed

+59
-21
lines changed

1 file changed

+59
-21
lines changed

notifyicon.go

Lines changed: 59 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,9 @@ func newNotificationIconWindow() (*notifyIconWindow, error) {
227230
niwCfg := windowCfg{
228231
Window: niw,
229232
ClassName: notifyIconWindowClass,
230-
Style: win.WS_OVERLAPPEDWINDOW,
233+
// Creating the window with WS_DISABLED in an effort to dissuade screen
234+
// readers from treating the hidden window as focusable content.
235+
Style: win.WS_OVERLAPPEDWINDOW | win.WS_DISABLED,
231236
// Always create the window at the origin, thus ensuring that the window
232237
// resides on the desktop's primary monitor, which is the same monitor where
233238
// the taskbar notification area resides. This ensures that the window's
@@ -239,6 +244,10 @@ func newNotificationIconWindow() (*notifyIconWindow, error) {
239244
if err := initWindowWithCfg(&niwCfg); err != nil {
240245
return nil, err
241246
}
247+
248+
// By default the window has the "client" role, which suggests content.
249+
// Assigning the "window" role instead.
250+
niw.Accessibility().SetRole(AccRoleWindow)
242251
return niw, nil
243252
}
244253

@@ -275,19 +284,10 @@ func newShellNotificationIcon(guid *windows.GUID) (*shellNotificationIcon, error
275284
return shellIcon, nil
276285
}
277286

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-
}
287+
// If we're using a GUID, an add operation can fail if a previous instance
288+
// using this GUID terminated abnormally and its notification icon was left
289+
// behind on the taskbar. Preemptively delete any pre-existing icon.
290+
shellIcon.clearAnyPreExisting()
291291

292292
// Add our notify icon to the status area and make sure it is hidden.
293293
addCmd := shellIcon.newCmd(win.NIM_ADD)
@@ -300,13 +300,32 @@ func newShellNotificationIcon(guid *windows.GUID) (*shellNotificationIcon, error
300300
return shellIcon, nil
301301
}
302302

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

312331
func (i *shellNotificationIcon) Dispose() error {
@@ -455,6 +474,13 @@ func (cmd *niCmd) setVisible(v bool) {
455474
}
456475

457476
func (cmd *niCmd) execute() error {
477+
var addShowTip bool
478+
if cmd.op == win.NIM_ADD && (cmd.nid.UFlags&win.NIF_SHOWTIP) != 0 {
479+
// NIF_SHOWTIP is a v4 flag. Don't include it in flags for NIM_ADD, which
480+
// is a v1 operation. We add it back in below, after we've upgraded to v4.
481+
addShowTip = true
482+
cmd.nid.UFlags ^= win.NIF_SHOWTIP
483+
}
458484
if !win.Shell_NotifyIcon(cmd.op, &cmd.nid) {
459485
return lastError(fmt.Sprintf("Shell_NotifyIcon(%d, %#v)", cmd.op, cmd.nid))
460486
}
@@ -473,7 +499,14 @@ func (cmd *niCmd) execute() error {
473499
verCmd.op = win.NIM_SETVERSION
474500
// Use Vista+ behaviour.
475501
verCmd.nid.UVersion = win.NOTIFYICON_VERSION_4
476-
return verCmd.execute()
502+
if err := verCmd.execute(); err != nil || !addShowTip {
503+
return err
504+
}
505+
506+
showTipCmd := *cmd
507+
showTipCmd.op = win.NIM_MODIFY
508+
showTipCmd.nid.UFlags |= win.NIF_SHOWTIP
509+
return showTipCmd.execute()
477510
}
478511

479512
// NotifyIcon represents an icon in the taskbar notification area.
@@ -551,6 +584,11 @@ func (ni *NotifyIcon) reAddToTaskbar() {
551584
// track this once the add command successfully executes.
552585
prevID := ni.shellIcon.id
553586

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

0 commit comments

Comments
 (0)