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
CUserNotification()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
~CUserNotification()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
RemoveIcon()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
DelayRemoveIcon(IN HRESULT hRes)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
TimeoutIcon()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
SetUpNotifyData(IN UINT uFlags,IN OUT PNOTIFYICONDATAW pnid)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
SetBalloonInfo(IN LPCWSTR pszTitle,IN LPCWSTR pszText,IN DWORD dwInfoFlags)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
SetBalloonRetry(IN DWORD dwShowTime,IN DWORD dwInterval,IN UINT cRetryCount)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
SetIconInfo(IN HICON hIcon,IN LPCWSTR pszToolTip)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
WorkerWndProc(IN HWND hWnd,IN UINT uMsg,IN WPARAM wParam,IN LPARAM lParam)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
Show(IN IQueryContinue * pqc,IN DWORD dwContinuePollInterval)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
PlaySound(IN LPCWSTR pszSoundName)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