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