1 /* 2 * Copyright 2018 Hermes Belusca-Maito 3 * 4 * Pass on icon notification messages to the systray implementation 5 * in the currently running shell. 6 * 7 * This library is free software; you can redistribute it and/or 8 * modify it under the terms of the GNU Lesser General Public 9 * License as published by the Free Software Foundation; either 10 * version 2.1 of the License, or (at your option) any later version. 11 * 12 * This library is distributed in the hope that it will be useful, 13 * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 * Lesser General Public License for more details. 16 * 17 * You should have received a copy of the GNU Lesser General Public 18 * License along with this library; if not, write to the Free Software 19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 20 */ 21 22 #include "precomp.h" 23 24 #include <mmsystem.h> 25 #undef PlaySound 26 27 WINE_DEFAULT_DEBUG_CHANNEL(shell_notify); 28 29 30 /* Use Windows-compatible window callback message */ 31 #define WM_TRAYNOTIFY (WM_USER + 100) 32 33 /* Notification icon ID */ 34 #define ID_NOTIFY_ICON 0 35 36 /* Balloon timers */ 37 #define ID_BALLOON_TIMEOUT 1 38 #define ID_BALLOON_DELAYREMOVE 2 39 #define ID_BALLOON_QUERYCONT 3 40 #define ID_BALLOON_SHOWTIME 4 41 42 #define BALLOON_DELAYREMOVE_TIMEOUT 250 // milliseconds 43 44 45 CUserNotification::CUserNotification() : 46 m_hWorkerWnd(NULL), 47 m_hIcon(NULL), 48 m_dwInfoFlags(0), 49 m_uShowTime(15000), 50 m_uInterval(10000), 51 m_cRetryCount(-1), 52 m_uContinuePoolInterval(0), 53 m_bIsShown(FALSE), 54 m_hRes(S_OK), 55 m_pqc(NULL) 56 { 57 } 58 59 CUserNotification::~CUserNotification() 60 { 61 /* If we have a notification window... */ 62 if (m_hWorkerWnd) 63 { 64 /* ... remove the notification icon and destroy the window */ 65 RemoveIcon(); 66 ::DestroyWindow(m_hWorkerWnd); 67 m_hWorkerWnd = NULL; 68 } 69 70 /* Destroy our local icon copy */ 71 if (m_hIcon) 72 ::DestroyIcon(m_hIcon); 73 } 74 75 VOID CUserNotification::RemoveIcon() 76 { 77 NOTIFYICONDATAW nid = {0}; 78 79 nid.cbSize = NOTIFYICONDATAW_V3_SIZE; // sizeof(nid); 80 nid.hWnd = m_hWorkerWnd; 81 nid.uID = ID_NOTIFY_ICON; 82 83 /* Remove the notification icon */ 84 ::Shell_NotifyIconW(NIM_DELETE, &nid); 85 } 86 87 VOID CUserNotification::DelayRemoveIcon(IN HRESULT hRes) 88 { 89 /* Set the return value for CUserNotification::Show() and defer icon removal */ 90 m_hRes = hRes; 91 ::SetTimer(m_hWorkerWnd, ID_BALLOON_DELAYREMOVE, 92 BALLOON_DELAYREMOVE_TIMEOUT, NULL); 93 } 94 95 VOID CUserNotification::TimeoutIcon() 96 { 97 /* 98 * The balloon timed out, we need to wait before showing it again. 99 * If we retried too many times, delete the notification icon. 100 */ 101 if (m_cRetryCount > 0) 102 { 103 /* Decrement the retry count */ 104 --m_cRetryCount; 105 106 /* Set the timeout interval timer */ 107 ::SetTimer(m_hWorkerWnd, ID_BALLOON_TIMEOUT, m_uInterval, NULL); 108 } 109 else 110 { 111 /* No other retry: delete the notification icon */ 112 DelayRemoveIcon(HRESULT_FROM_WIN32(ERROR_CANCELLED)); 113 } 114 } 115 116 VOID CUserNotification::SetUpNotifyData( 117 IN UINT uFlags, 118 IN OUT PNOTIFYICONDATAW pnid) 119 { 120 pnid->cbSize = NOTIFYICONDATAW_V3_SIZE; // sizeof(nid); 121 pnid->hWnd = m_hWorkerWnd; 122 pnid->uID = ID_NOTIFY_ICON; 123 // pnid->uVersion = NOTIFYICON_VERSION; 124 125 if (uFlags & NIF_MESSAGE) 126 { 127 pnid->uFlags |= NIF_MESSAGE; 128 pnid->uCallbackMessage = WM_TRAYNOTIFY; 129 } 130 131 if (uFlags & NIF_ICON) 132 { 133 pnid->uFlags |= NIF_ICON; 134 /* Use a default icon if we do not have one already */ 135 pnid->hIcon = (m_hIcon ? m_hIcon : LoadIcon(NULL, IDI_WINLOGO)); 136 } 137 138 if (uFlags & NIF_TIP) 139 { 140 pnid->uFlags |= NIF_TIP; 141 ::StringCchCopyW(pnid->szTip, _countof(pnid->szTip), m_szTip); 142 } 143 144 if (uFlags & NIF_INFO) 145 { 146 pnid->uFlags |= NIF_INFO; 147 148 // pnid->uTimeout = m_uShowTime; // NOTE: Deprecated 149 pnid->dwInfoFlags = m_dwInfoFlags; 150 151 ::StringCchCopyW(pnid->szInfo, _countof(pnid->szInfo), m_szInfo); 152 ::StringCchCopyW(pnid->szInfoTitle, _countof(pnid->szInfoTitle), m_szInfoTitle); 153 } 154 } 155 156 157 /* IUserNotification Implementation */ 158 159 HRESULT STDMETHODCALLTYPE 160 CUserNotification::SetBalloonInfo( 161 IN LPCWSTR pszTitle, 162 IN LPCWSTR pszText, 163 IN DWORD dwInfoFlags) 164 { 165 NOTIFYICONDATAW nid = {0}; 166 167 m_szInfo = pszText; 168 m_szInfoTitle = pszTitle; 169 m_dwInfoFlags = dwInfoFlags; 170 171 /* Update the notification icon if we have one */ 172 if (!m_hWorkerWnd) 173 return S_OK; 174 175 /* Modify the notification icon */ 176 SetUpNotifyData(NIF_INFO, &nid); 177 if (::Shell_NotifyIconW(NIM_MODIFY, &nid)) 178 return S_OK; 179 else 180 return E_FAIL; 181 } 182 183 HRESULT STDMETHODCALLTYPE 184 CUserNotification::SetBalloonRetry( 185 IN DWORD dwShowTime, // Time intervals in milliseconds 186 IN DWORD dwInterval, 187 IN UINT cRetryCount) 188 { 189 m_uShowTime = dwShowTime; 190 m_uInterval = dwInterval; 191 m_cRetryCount = cRetryCount; 192 return S_OK; 193 } 194 195 HRESULT STDMETHODCALLTYPE 196 CUserNotification::SetIconInfo( 197 IN HICON hIcon, 198 IN LPCWSTR pszToolTip) 199 { 200 NOTIFYICONDATAW nid = {0}; 201 202 /* Destroy our local icon copy */ 203 if (m_hIcon) 204 ::DestroyIcon(m_hIcon); 205 206 if (hIcon) 207 { 208 /* Copy the icon from the user */ 209 m_hIcon = ::CopyIcon(hIcon); 210 } 211 else 212 { 213 /* Use the same icon as the one for the balloon if specified */ 214 UINT uIcon = (m_dwInfoFlags & NIIF_ICON_MASK); 215 LPCWSTR pIcon = NULL; 216 217 if (uIcon == NIIF_INFO) 218 pIcon = IDI_INFORMATION; 219 else if (uIcon == NIIF_WARNING) 220 pIcon = IDI_WARNING; 221 else if (uIcon == NIIF_ERROR) 222 pIcon = IDI_ERROR; 223 else if (uIcon == NIIF_USER) 224 pIcon = NULL; 225 226 m_hIcon = (pIcon ? ::LoadIconW(NULL, pIcon) : NULL); 227 } 228 229 m_szTip = pszToolTip; 230 231 /* Update the notification icon if we have one */ 232 if (!m_hWorkerWnd) 233 return S_OK; 234 235 /* Modify the notification icon */ 236 SetUpNotifyData(NIF_ICON | NIF_TIP, &nid); 237 if (::Shell_NotifyIconW(NIM_MODIFY, &nid)) 238 return S_OK; 239 else 240 return E_FAIL; 241 } 242 243 244 LRESULT CALLBACK 245 CUserNotification::WorkerWndProc( 246 IN HWND hWnd, 247 IN UINT uMsg, 248 IN WPARAM wParam, 249 IN LPARAM lParam) 250 { 251 /* Retrieve the current user notification object stored in the window extra bits */ 252 CUserNotification* pThis = reinterpret_cast<CUserNotification*>(::GetWindowLongPtrW(hWnd, 0)); 253 ASSERT(pThis); 254 ASSERT(hWnd == pThis->m_hWorkerWnd); 255 256 TRACE("Msg = 0x%x\n", uMsg); 257 switch (uMsg) 258 { 259 /* 260 * We do not receive any WM_(NC)CREATE message since worker windows 261 * are first created using the default window procedure DefWindowProcW. 262 * The window procedure is changed only subsequently to the user one. 263 * We however receive WM_(NC)DESTROY messages. 264 */ 265 case WM_DESTROY: 266 { 267 /* Post a WM_QUIT message only if the Show() method's message loop is running */ 268 if (pThis->m_bIsShown) 269 ::PostQuitMessage(0); 270 return 0; 271 } 272 273 case WM_NCDESTROY: 274 { 275 ::SetWindowLongPtrW(hWnd, 0, (LONG_PTR)NULL); 276 pThis->m_hWorkerWnd = NULL; 277 return 0; 278 } 279 280 case WM_QUERYENDSESSION: 281 { 282 /* 283 * User session is ending or a shutdown is occurring: perform cleanup. 284 * Set the return value for CUserNotification::Show() and remove the notification. 285 */ 286 pThis->m_hRes = HRESULT_FROM_WIN32(ERROR_CANCELLED); 287 pThis->RemoveIcon(); 288 ::DestroyWindow(pThis->m_hWorkerWnd); 289 return TRUE; 290 } 291 292 case WM_TIMER: 293 { 294 TRACE("WM_TIMER(0x%lx)\n", wParam); 295 296 /* Destroy the associated timer */ 297 ::KillTimer(hWnd, (UINT_PTR)wParam); 298 299 if (wParam == ID_BALLOON_TIMEOUT) 300 { 301 /* Timeout interval timer expired: display the balloon again */ 302 NOTIFYICONDATAW nid = {0}; 303 pThis->SetUpNotifyData(NIF_INFO, &nid); 304 ::Shell_NotifyIconW(NIM_MODIFY, &nid); 305 } 306 else if (wParam == ID_BALLOON_DELAYREMOVE) 307 { 308 /* Delay-remove timer expired: remove the notification */ 309 pThis->RemoveIcon(); 310 ::DestroyWindow(pThis->m_hWorkerWnd); 311 } 312 else if (wParam == ID_BALLOON_QUERYCONT) 313 { 314 /* 315 * Query-continue timer expired: ask the user whether the 316 * notification should continue to be displayed or not. 317 */ 318 if (pThis->m_pqc && pThis->m_pqc->QueryContinue() == S_OK) 319 { 320 /* The notification can be displayed */ 321 ::SetTimer(hWnd, ID_BALLOON_QUERYCONT, pThis->m_uContinuePoolInterval, NULL); 322 } 323 else 324 { 325 /* The notification should be removed */ 326 pThis->DelayRemoveIcon(S_FALSE); 327 } 328 } 329 else if (wParam == ID_BALLOON_SHOWTIME) 330 { 331 /* Show-time timer expired: wait before showing the balloon again */ 332 pThis->TimeoutIcon(); 333 } 334 return 0; 335 } 336 337 /* 338 * Shell User Notification message. 339 * We use NOTIFYICON_VERSION == 0 or 3 callback version, with: 340 * wParam == identifier of the taskbar icon in which the event occurred; 341 * lParam == holds the mouse or keyboard message associated with the event. 342 */ 343 case WM_TRAYNOTIFY: 344 { 345 TRACE("WM_TRAYNOTIFY - wParam = 0x%lx ; lParam = 0x%lx\n", wParam, lParam); 346 ASSERT(wParam == ID_NOTIFY_ICON); 347 348 switch (lParam) 349 { 350 case NIN_BALLOONSHOW: 351 TRACE("NIN_BALLOONSHOW\n"); 352 break; 353 354 case NIN_BALLOONHIDE: 355 TRACE("NIN_BALLOONHIDE\n"); 356 break; 357 358 /* The balloon timed out, or the user closed it by clicking on the 'X' button */ 359 case NIN_BALLOONTIMEOUT: 360 { 361 TRACE("NIN_BALLOONTIMEOUT\n"); 362 pThis->TimeoutIcon(); 363 break; 364 } 365 366 /* The user clicked on the balloon: delete the notification icon */ 367 case NIN_BALLOONUSERCLICK: 368 TRACE("NIN_BALLOONUSERCLICK\n"); 369 /* Fall back to icon click behaviour */ 370 371 /* The user clicked on the notification icon: delete it */ 372 case WM_LBUTTONDOWN: 373 case WM_RBUTTONDOWN: 374 { 375 pThis->DelayRemoveIcon(S_OK); 376 break; 377 } 378 379 default: 380 break; 381 } 382 383 return 0; 384 } 385 } 386 387 return ::DefWindowProcW(hWnd, uMsg, wParam, lParam); 388 } 389 390 391 // Blocks until the notification times out. 392 HRESULT STDMETHODCALLTYPE 393 CUserNotification::Show( 394 IN IQueryContinue* pqc, 395 IN DWORD dwContinuePollInterval) 396 { 397 NOTIFYICONDATAW nid = {0}; 398 MSG msg; 399 400 /* Create the hidden notification message worker window if we do not have one already */ 401 if (!m_hWorkerWnd) 402 { 403 m_hWorkerWnd = ::SHCreateWorkerWindowW(CUserNotification::WorkerWndProc, 404 NULL, 0, 0, NULL, (LONG_PTR)this); 405 if (!m_hWorkerWnd) 406 { 407 FAILED_UNEXPECTEDLY(E_FAIL); 408 return E_FAIL; 409 } 410 411 /* Add and display the notification icon */ 412 SetUpNotifyData(NIF_MESSAGE | NIF_ICON | NIF_TIP | NIF_INFO, &nid); 413 if (!::Shell_NotifyIconW(NIM_ADD, &nid)) 414 { 415 ::DestroyWindow(m_hWorkerWnd); 416 m_hWorkerWnd = NULL; 417 return E_FAIL; 418 } 419 } 420 421 m_hRes = S_OK; 422 423 /* Set up the user-continue callback mechanism */ 424 m_pqc = pqc; 425 if (pqc) 426 { 427 m_uContinuePoolInterval = dwContinuePollInterval; 428 ::SetTimer(m_hWorkerWnd, ID_BALLOON_QUERYCONT, m_uContinuePoolInterval, NULL); 429 } 430 431 /* Control how long the balloon notification is displayed */ 432 if ((nid.uFlags & NIF_INFO) && !*nid.szInfo /* && !*nid.szInfoTitle */) 433 ::SetTimer(m_hWorkerWnd, ID_BALLOON_SHOWTIME, m_uShowTime, NULL); 434 435 /* Dispatch messsages to the worker window */ 436 m_bIsShown = TRUE; 437 while (::GetMessageW(&msg, NULL, 0, 0)) 438 { 439 ::TranslateMessage(&msg); 440 ::DispatchMessageW(&msg); 441 } 442 m_bIsShown = FALSE; 443 444 /* Reset the user-continue callback mechanism */ 445 if (pqc) 446 { 447 ::KillTimer(m_hWorkerWnd, ID_BALLOON_QUERYCONT); 448 m_uContinuePoolInterval = 0; 449 } 450 m_pqc = NULL; 451 452 /* Return the notification error code */ 453 return m_hRes; 454 } 455 456 #if 0 // IUserNotification2 457 // Blocks until the notification times out. 458 HRESULT STDMETHODCALLTYPE 459 CUserNotification::Show( 460 IN IQueryContinue* pqc, 461 IN DWORD dwContinuePollInterval, 462 IN IUserNotificationCallback* pSink) 463 { 464 return S_OK; 465 } 466 #endif 467 468 HRESULT STDMETHODCALLTYPE 469 CUserNotification::PlaySound( 470 IN LPCWSTR pszSoundName) 471 { 472 /* Call the Win32 API - Ignore the PlaySoundW() return value as on Windows */ 473 ::PlaySoundW(pszSoundName, 474 NULL, 475 SND_ALIAS | SND_APPLICATION | 476 SND_NOSTOP | SND_NODEFAULT | SND_ASYNC); 477 return S_OK; 478 } 479