1 /** 2 * System tray (notification area) icons. 3 * 4 * `TrayIcon` wraps `Shell_NotifyIconW`. It shows an icon with a tooltip in the 5 * notification area, optionally a balloon notification and a right-click context 6 * menu, and fires `onDoubleClicked` when the icon is double-clicked (the usual 7 * "restore the window" gesture). 8 * 9 * The icon sends its mouse notifications to the owner window as a private 10 * application message; `Window` routes that message here via 11 * `dispatchTrayMessage`. Call `remove()` before the owner window is torn down — 12 * a lingering tray icon can leave screen-reader focus in a bad state. 13 */ 14 module deft.controls.trayicon; 15 16 version (Windows): 17 18 import core.sys.windows.windows; 19 import core.sys.windows.shellapi; 20 21 import deft.events; 22 import deft.menu : Menu; 23 import deft.util.strings; 24 import deft.window : Window; 25 26 /// Private window message the tray icon uses to report mouse activity. 27 enum UINT trayCallbackMessage = WM_APP + 100; 28 29 private __gshared TrayIcon[uint] g_trayIcons; 30 private __gshared uint g_nextTrayId = 1; 31 32 /** 33 * Route a tray callback message to the matching `TrayIcon`. 34 * 35 * `id` is the icon id (the `wParam` of the callback message); `mouseMsg` is the 36 * mouse message (`lParam`). Returns `true` if a matching icon handled it. 37 */ 38 bool dispatchTrayMessage(uint id, uint mouseMsg) 39 { 40 if (auto icon = id in g_trayIcons) 41 { 42 (*icon).handleMouse(mouseMsg); 43 return true; 44 } 45 return false; 46 } 47 48 /// Copy a D string into a fixed-size wide buffer, NUL-terminating and clipping. 49 private void copyToWBuffer(string s, WCHAR[] dest) 50 { 51 auto src = s.toWStringz; 52 size_t i = 0; 53 for (; i + 1 < dest.length && src[i] != '\0'; ++i) 54 dest[i] = src[i]; 55 dest[i] = '\0'; 56 } 57 58 /// A notification-area icon owned by a top-level window. 59 class TrayIcon 60 { 61 private Window owner_; 62 private uint id_; 63 private HICON icon_; 64 private string tooltip_; 65 private Menu contextMenu_; 66 private bool added_; 67 68 /// Fired when the icon is double-clicked with the left mouse button. 69 Event!() onDoubleClicked; 70 71 /** 72 * Create a tray icon for `owner` with the given tooltip. 73 * 74 * The icon is added to the notification area immediately; set an icon image 75 * with `setIcon` (until then the area shows a blank slot). 76 */ 77 this(Window owner, string tooltip) 78 { 79 owner_ = owner; 80 tooltip_ = tooltip; 81 id_ = g_nextTrayId++; 82 g_trayIcons[id_] = this; 83 add(); 84 } 85 86 private NOTIFYICONDATAW baseData() 87 { 88 NOTIFYICONDATAW nid; 89 nid.cbSize = NOTIFYICONDATAW.sizeof; 90 nid.hWnd = owner_.handle; 91 nid.uID = id_; 92 return nid; 93 } 94 95 private void add() 96 { 97 auto nid = baseData(); 98 nid.uFlags = NIF_MESSAGE | NIF_TIP; 99 nid.uCallbackMessage = trayCallbackMessage; 100 copyToWBuffer(tooltip_, nid.szTip[]); 101 if (icon_ !is null) 102 { 103 nid.uFlags |= NIF_ICON; 104 nid.hIcon = icon_; 105 } 106 Shell_NotifyIconW(NIM_ADD, &nid); 107 added_ = true; 108 } 109 110 /// Set the icon image shown in the notification area. 111 void setIcon(HICON icon) 112 { 113 icon_ = icon; 114 auto nid = baseData(); 115 nid.uFlags = NIF_ICON; 116 nid.hIcon = icon; 117 Shell_NotifyIconW(NIM_MODIFY, &nid); 118 } 119 120 /// Change the hover tooltip text. 121 void setTooltip(string text) 122 { 123 tooltip_ = text; 124 auto nid = baseData(); 125 nid.uFlags = NIF_TIP; 126 copyToWBuffer(text, nid.szTip[]); 127 Shell_NotifyIconW(NIM_MODIFY, &nid); 128 } 129 130 /// Show a balloon notification with `title` and `text`. 131 void showBalloon(string title, string text) 132 { 133 auto nid = baseData(); 134 nid.uFlags = NIF_INFO; 135 copyToWBuffer(title, nid.szInfoTitle[]); 136 copyToWBuffer(text, nid.szInfo[]); 137 Shell_NotifyIconW(NIM_MODIFY, &nid); 138 } 139 140 /// Set the menu shown on right-click (or Apps key while the icon is active). 141 void setContextMenu(Menu menu) 142 { 143 contextMenu_ = menu; 144 } 145 146 /** 147 * Remove the icon from the notification area. 148 * 149 * Call this before destroying the owner window. Idempotent. (Named `remove` 150 * rather than `destroy` to avoid shadowing druntime's built-in `destroy`.) 151 */ 152 void remove() 153 { 154 if (!added_) 155 return; 156 auto nid = baseData(); 157 Shell_NotifyIconW(NIM_DELETE, &nid); 158 g_trayIcons.remove(id_); 159 added_ = false; 160 } 161 162 /// React to a mouse message forwarded from the owner window. 163 private void handleMouse(uint mouseMsg) 164 { 165 switch (mouseMsg) 166 { 167 case WM_LBUTTONDBLCLK: 168 onDoubleClicked.fire(); 169 break; 170 171 case WM_RBUTTONUP: 172 case WM_CONTEXTMENU: 173 if (contextMenu_ !is null && owner_ !is null && owner_.handle !is null) 174 { 175 POINT pt; 176 GetCursorPos(&pt); 177 // Foreground + the trailing null post are the documented fix for 178 // a tray menu that otherwise won't dismiss on an outside click. 179 SetForegroundWindow(owner_.handle); 180 TrackPopupMenu(contextMenu_.handle, 181 TPM_LEFTALIGN | TPM_RIGHTBUTTON, 182 pt.x, pt.y, 0, owner_.handle, null); 183 PostMessageW(owner_.handle, WM_NULL, 0, 0); 184 } 185 break; 186 187 default: 188 break; 189 } 190 } 191 }