diff --git a/dll/win32/shell32/CMakeLists.txt b/dll/win32/shell32/CMakeLists.txt index 8bdf3ee56ff..76ebf226e30 100644 --- a/dll/win32/shell32/CMakeLists.txt +++ b/dll/win32/shell32/CMakeLists.txt @@ -67,6 +67,7 @@ list(APPEND SOURCE CDefViewBckgrndMenu.cpp stubs.cpp systray.cpp + CUserNotification.cpp CDefaultContextMenu.cpp COpenWithMenu.cpp CNewMenu.cpp diff --git a/dll/win32/shell32/CUserNotification.cpp b/dll/win32/shell32/CUserNotification.cpp new file mode 100644 index 00000000000..c76bd0b10f2 --- /dev/null +++ b/dll/win32/shell32/CUserNotification.cpp @@ -0,0 +1,478 @@ +/* + * Copyright 2018 Hermes Belusca-Maito + * + * Pass on icon notification messages to the systray implementation + * in the currently running shell. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "precomp.h" + +#include +#undef PlaySound + +WINE_DEFAULT_DEBUG_CHANNEL(shell_notify); + + +/* Use Windows-compatible window callback message */ +#define WM_TRAYNOTIFY (WM_USER + 100) + +/* Notification icon ID */ +#define ID_NOTIFY_ICON 0 + +/* Balloon timers */ +#define ID_BALLOON_TIMEOUT 1 +#define ID_BALLOON_DELAYREMOVE 2 +#define ID_BALLOON_QUERYCONT 3 +#define ID_BALLOON_SHOWTIME 4 + +#define BALLOON_DELAYREMOVE_TIMEOUT 250 // milliseconds + + +CUserNotification::CUserNotification() : + m_hWorkerWnd(NULL), + m_hIcon(NULL), + m_dwInfoFlags(0), + m_uShowTime(15000), + m_uInterval(10000), + m_cRetryCount(-1), + m_uContinuePoolInterval(0), + m_bIsShown(FALSE), + m_hRes(S_OK), + m_pqc(NULL) +{ +} + +CUserNotification::~CUserNotification() +{ + /* If we have a notification window... */ + if (m_hWorkerWnd) + { + /* ... remove the notification icon and destroy the window */ + RemoveIcon(); + ::DestroyWindow(m_hWorkerWnd); + m_hWorkerWnd = NULL; + } + + /* Destroy our local icon copy */ + if (m_hIcon) + ::DestroyIcon(m_hIcon); +} + +VOID CUserNotification::RemoveIcon() +{ + NOTIFYICONDATAW nid = {0}; + + nid.cbSize = NOTIFYICONDATAW_V3_SIZE; // sizeof(nid); + nid.hWnd = m_hWorkerWnd; + nid.uID = ID_NOTIFY_ICON; + + /* Remove the notification icon */ + ::Shell_NotifyIconW(NIM_DELETE, &nid); +} + +VOID CUserNotification::DelayRemoveIcon(IN HRESULT hRes) +{ + /* Set the return value for CUserNotification::Show() and defer icon removal */ + m_hRes = hRes; + ::SetTimer(m_hWorkerWnd, ID_BALLOON_DELAYREMOVE, + BALLOON_DELAYREMOVE_TIMEOUT, NULL); +} + +VOID CUserNotification::TimeoutIcon() +{ + /* + * The balloon timed out, we need to wait before showing it again. + * If we retried too many times, delete the notification icon. + */ + if (m_cRetryCount > 0) + { + /* Decrement the retry count */ + --m_cRetryCount; + + /* Set the timeout interval timer */ + ::SetTimer(m_hWorkerWnd, ID_BALLOON_TIMEOUT, m_uInterval, NULL); + } + else + { + /* No other retry: delete the notification icon */ + DelayRemoveIcon(HRESULT_FROM_WIN32(ERROR_CANCELLED)); + } +} + +VOID CUserNotification::SetUpNotifyData( + IN UINT uFlags, + IN OUT PNOTIFYICONDATAW pnid) +{ + pnid->cbSize = NOTIFYICONDATAW_V3_SIZE; // sizeof(nid); + pnid->hWnd = m_hWorkerWnd; + pnid->uID = ID_NOTIFY_ICON; + // pnid->uVersion = NOTIFYICON_VERSION; + + if (uFlags & NIF_MESSAGE) + { + pnid->uFlags |= NIF_MESSAGE; + pnid->uCallbackMessage = WM_TRAYNOTIFY; + } + + if (uFlags & NIF_ICON) + { + pnid->uFlags |= NIF_ICON; + /* Use a default icon if we do not have one already */ + pnid->hIcon = (m_hIcon ? m_hIcon : LoadIcon(NULL, IDI_WINLOGO)); + } + + if (uFlags & NIF_TIP) + { + pnid->uFlags |= NIF_TIP; + ::StringCchCopyW(pnid->szTip, _countof(pnid->szTip), m_szTip); + } + + if (uFlags & NIF_INFO) + { + pnid->uFlags |= NIF_INFO; + + // pnid->uTimeout = m_uShowTime; // NOTE: Deprecated + pnid->dwInfoFlags = m_dwInfoFlags; + + ::StringCchCopyW(pnid->szInfo, _countof(pnid->szInfo), m_szInfo); + ::StringCchCopyW(pnid->szInfoTitle, _countof(pnid->szInfoTitle), m_szInfoTitle); + } +} + + +/* IUserNotification Implementation */ + +HRESULT STDMETHODCALLTYPE +CUserNotification::SetBalloonInfo( + IN LPCWSTR pszTitle, + IN LPCWSTR pszText, + IN DWORD dwInfoFlags) +{ + NOTIFYICONDATAW nid = {0}; + + m_szInfo = pszText; + m_szInfoTitle = pszTitle; + m_dwInfoFlags = dwInfoFlags; + + /* Update the notification icon if we have one */ + if (!m_hWorkerWnd) + return S_OK; + + /* Modify the notification icon */ + SetUpNotifyData(NIF_INFO, &nid); + if (::Shell_NotifyIconW(NIM_MODIFY, &nid)) + return S_OK; + else + return E_FAIL; +} + +HRESULT STDMETHODCALLTYPE +CUserNotification::SetBalloonRetry( + IN DWORD dwShowTime, // Time intervals in milliseconds + IN DWORD dwInterval, + IN UINT cRetryCount) +{ + m_uShowTime = dwShowTime; + m_uInterval = dwInterval; + m_cRetryCount = cRetryCount; + return S_OK; +} + +HRESULT STDMETHODCALLTYPE +CUserNotification::SetIconInfo( + IN HICON hIcon, + IN LPCWSTR pszToolTip) +{ + NOTIFYICONDATAW nid = {0}; + + /* Destroy our local icon copy */ + if (m_hIcon) + ::DestroyIcon(m_hIcon); + + if (hIcon) + { + /* Copy the icon from the user */ + m_hIcon = ::CopyIcon(hIcon); + } + else + { + /* Use the same icon as the one for the balloon if specified */ + UINT uIcon = (m_dwInfoFlags & NIIF_ICON_MASK); + LPCWSTR pIcon = NULL; + + if (uIcon == NIIF_INFO) + pIcon = IDI_INFORMATION; + else if (uIcon == NIIF_WARNING) + pIcon = IDI_WARNING; + else if (uIcon == NIIF_ERROR) + pIcon = IDI_ERROR; + else if (uIcon == NIIF_USER) + pIcon = NULL; + + m_hIcon = (pIcon ? ::LoadIconW(NULL, pIcon) : NULL); + } + + m_szTip = pszToolTip; + + /* Update the notification icon if we have one */ + if (!m_hWorkerWnd) + return S_OK; + + /* Modify the notification icon */ + SetUpNotifyData(NIF_ICON | NIF_TIP, &nid); + if (::Shell_NotifyIconW(NIM_MODIFY, &nid)) + return S_OK; + else + return E_FAIL; +} + + +LRESULT CALLBACK +CUserNotification::WorkerWndProc( + IN HWND hWnd, + IN UINT uMsg, + IN WPARAM wParam, + IN LPARAM lParam) +{ + /* Retrieve the current user notification object stored in the window extra bits */ + CUserNotification* pThis = reinterpret_cast(::GetWindowLongPtrW(hWnd, 0)); + ASSERT(pThis); + ASSERT(hWnd == pThis->m_hWorkerWnd); + + TRACE("Msg = 0x%x\n", uMsg); + switch (uMsg) + { + /* + * We do not receive any WM_(NC)CREATE message since worker windows + * are first created using the default window procedure DefWindowProcW. + * The window procedure is changed only subsequently to the user one. + * We however receive WM_(NC)DESTROY messages. + */ + case WM_DESTROY: + { + /* Post a WM_QUIT message only if the Show() method's message loop is running */ + if (pThis->m_bIsShown) + ::PostQuitMessage(0); + return 0; + } + + case WM_NCDESTROY: + { + ::SetWindowLongPtrW(hWnd, 0, (LONG_PTR)NULL); + pThis->m_hWorkerWnd = NULL; + return 0; + } + + case WM_QUERYENDSESSION: + { + /* + * User session is ending or a shutdown is occurring: perform cleanup. + * Set the return value for CUserNotification::Show() and remove the notification. + */ + pThis->m_hRes = HRESULT_FROM_WIN32(ERROR_CANCELLED); + pThis->RemoveIcon(); + ::DestroyWindow(pThis->m_hWorkerWnd); + return TRUE; + } + + case WM_TIMER: + { + TRACE("WM_TIMER(0x%lx)\n", wParam); + + /* Destroy the associated timer */ + ::KillTimer(hWnd, (UINT_PTR)wParam); + + if (wParam == ID_BALLOON_TIMEOUT) + { + /* Timeout interval timer expired: display the balloon again */ + NOTIFYICONDATAW nid = {0}; + pThis->SetUpNotifyData(NIF_INFO, &nid); + ::Shell_NotifyIconW(NIM_MODIFY, &nid); + } + else if (wParam == ID_BALLOON_DELAYREMOVE) + { + /* Delay-remove timer expired: remove the notification */ + pThis->RemoveIcon(); + ::DestroyWindow(pThis->m_hWorkerWnd); + } + else if (wParam == ID_BALLOON_QUERYCONT) + { + /* + * Query-continue timer expired: ask the user whether the + * notification should continue to be displayed or not. + */ + if (pThis->m_pqc && pThis->m_pqc->QueryContinue() == S_OK) + { + /* The notification can be displayed */ + ::SetTimer(hWnd, ID_BALLOON_QUERYCONT, pThis->m_uContinuePoolInterval, NULL); + } + else + { + /* The notification should be removed */ + pThis->DelayRemoveIcon(S_FALSE); + } + } + else if (wParam == ID_BALLOON_SHOWTIME) + { + /* Show-time timer expired: wait before showing the balloon again */ + pThis->TimeoutIcon(); + } + return 0; + } + + /* + * Shell User Notification message. + * We use NOTIFYICON_VERSION == 0 or 3 callback version, with: + * wParam == identifier of the taskbar icon in which the event occurred; + * lParam == holds the mouse or keyboard message associated with the event. + */ + case WM_TRAYNOTIFY: + { + TRACE("WM_TRAYNOTIFY - wParam = 0x%lx ; lParam = 0x%lx\n", wParam, lParam); + ASSERT(wParam == ID_NOTIFY_ICON); + + switch (lParam) + { + case NIN_BALLOONSHOW: + TRACE("NIN_BALLOONSHOW\n"); + break; + + case NIN_BALLOONHIDE: + TRACE("NIN_BALLOONHIDE\n"); + break; + + /* The balloon timed out, or the user closed it by clicking on the 'X' button */ + case NIN_BALLOONTIMEOUT: + { + TRACE("NIN_BALLOONTIMEOUT\n"); + pThis->TimeoutIcon(); + break; + } + + /* The user clicked on the balloon: delete the notification icon */ + case NIN_BALLOONUSERCLICK: + TRACE("NIN_BALLOONUSERCLICK\n"); + /* Fall back to icon click behaviour */ + + /* The user clicked on the notification icon: delete it */ + case WM_LBUTTONDOWN: + case WM_RBUTTONDOWN: + { + pThis->DelayRemoveIcon(S_OK); + break; + } + + default: + break; + } + + return 0; + } + } + + return ::DefWindowProcW(hWnd, uMsg, wParam, lParam); +} + + +// Blocks until the notification times out. +HRESULT STDMETHODCALLTYPE +CUserNotification::Show( + IN IQueryContinue* pqc, + IN DWORD dwContinuePollInterval) +{ + NOTIFYICONDATAW nid = {0}; + MSG msg; + + /* Create the hidden notification message worker window if we do not have one already */ + if (!m_hWorkerWnd) + { + m_hWorkerWnd = ::SHCreateWorkerWindowW(CUserNotification::WorkerWndProc, + NULL, 0, 0, NULL, (LONG_PTR)this); + if (!m_hWorkerWnd) + { + FAILED_UNEXPECTEDLY(E_FAIL); + return E_FAIL; + } + + /* Add and display the notification icon */ + SetUpNotifyData(NIF_MESSAGE | NIF_ICON | NIF_TIP | NIF_INFO, &nid); + if (!::Shell_NotifyIconW(NIM_ADD, &nid)) + { + ::DestroyWindow(m_hWorkerWnd); + m_hWorkerWnd = NULL; + return E_FAIL; + } + } + + m_hRes = S_OK; + + /* Set up the user-continue callback mechanism */ + m_pqc = pqc; + if (pqc) + { + m_uContinuePoolInterval = dwContinuePollInterval; + ::SetTimer(m_hWorkerWnd, ID_BALLOON_QUERYCONT, m_uContinuePoolInterval, NULL); + } + + /* Control how long the balloon notification is displayed */ + if ((nid.uFlags & NIF_INFO) && !*nid.szInfo /* && !*nid.szInfoTitle */) + ::SetTimer(m_hWorkerWnd, ID_BALLOON_SHOWTIME, m_uShowTime, NULL); + + /* Dispatch messsages to the worker window */ + m_bIsShown = TRUE; + while (::GetMessageW(&msg, NULL, 0, 0)) + { + ::TranslateMessage(&msg); + ::DispatchMessageW(&msg); + } + m_bIsShown = FALSE; + + /* Reset the user-continue callback mechanism */ + if (pqc) + { + ::KillTimer(m_hWorkerWnd, ID_BALLOON_QUERYCONT); + m_uContinuePoolInterval = 0; + } + m_pqc = NULL; + + /* Return the notification error code */ + return m_hRes; +} + +#if 0 // IUserNotification2 +// Blocks until the notification times out. +HRESULT STDMETHODCALLTYPE +CUserNotification::Show( + IN IQueryContinue* pqc, + IN DWORD dwContinuePollInterval, + IN IUserNotificationCallback* pSink) +{ + return S_OK; +} +#endif + +HRESULT STDMETHODCALLTYPE +CUserNotification::PlaySound( + IN LPCWSTR pszSoundName) +{ + /* Call the Win32 API - Ignore the PlaySoundW() return value as on Windows */ + ::PlaySoundW(pszSoundName, + NULL, + SND_ALIAS | SND_APPLICATION | + SND_NOSTOP | SND_NODEFAULT | SND_ASYNC); + return S_OK; +} diff --git a/dll/win32/shell32/CUserNotification.h b/dll/win32/shell32/CUserNotification.h new file mode 100644 index 00000000000..4f01e0c9be8 --- /dev/null +++ b/dll/win32/shell32/CUserNotification.h @@ -0,0 +1,111 @@ +/* + * Copyright 2018 Hermes Belusca-Maito + * + * Pass on icon notification messages to the systray implementation + * in the currently running shell. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef _USERNOTIFICATION_H_ +#define _USERNOTIFICATION_H_ + +#undef PlaySound + +class CUserNotification : + public CComCoClass, + public CComObjectRootEx, + public IUserNotification +// public IUserNotification2 // On Vista+ +{ +private: + HWND m_hWorkerWnd; + HICON m_hIcon; + DWORD m_dwInfoFlags; + UINT m_uShowTime; + UINT m_uInterval; + UINT m_cRetryCount; + UINT m_uContinuePoolInterval; + BOOL m_bIsShown; + HRESULT m_hRes; + IQueryContinue* m_pqc; + CStringW m_szTip; + CStringW m_szInfo; + CStringW m_szInfoTitle; + +private: + VOID RemoveIcon(); + VOID DelayRemoveIcon(IN HRESULT hRes); + VOID TimeoutIcon(); + + VOID SetUpNotifyData( + IN UINT uFlags, + IN OUT PNOTIFYICONDATAW pnid); + + static LRESULT CALLBACK + WorkerWndProc( + IN HWND hWnd, + IN UINT uMsg, + IN WPARAM wParam, + IN LPARAM lParam); + +public: + CUserNotification(); + ~CUserNotification(); + + // IUserNotification + virtual HRESULT STDMETHODCALLTYPE SetBalloonInfo( + IN LPCWSTR pszTitle, + IN LPCWSTR pszText, + IN DWORD dwInfoFlags); + + virtual HRESULT STDMETHODCALLTYPE SetBalloonRetry( + IN DWORD dwShowTime, // Time intervals in milliseconds + IN DWORD dwInterval, + IN UINT cRetryCount); + + virtual HRESULT STDMETHODCALLTYPE SetIconInfo( + IN HICON hIcon, + IN LPCWSTR pszToolTip); + + // Blocks until the notification times out. + virtual HRESULT STDMETHODCALLTYPE Show( + IN IQueryContinue* pqc, + IN DWORD dwContinuePollInterval); + + virtual HRESULT STDMETHODCALLTYPE PlaySound( + IN LPCWSTR pszSoundName); + +#if 0 + // IUserNotification2 + // Blocks until the notification times out. + virtual HRESULT STDMETHODCALLTYPE Show( + IN IQueryContinue* pqc, + IN DWORD dwContinuePollInterval, + IN IUserNotificationCallback* pSink); +#endif + + DECLARE_REGISTRY_RESOURCEID(IDR_USERNOTIFICATION) + DECLARE_NOT_AGGREGATABLE(CUserNotification) + + DECLARE_PROTECT_FINAL_CONSTRUCT() + + BEGIN_COM_MAP(CUserNotification) + COM_INTERFACE_ENTRY_IID(IID_IUserNotification , IUserNotification ) + // COM_INTERFACE_ENTRY_IID(IID_IUserNotification2, IUserNotification2) + END_COM_MAP() +}; + +#endif /* _USERNOTIFICATION_H_ */ diff --git a/dll/win32/shell32/precomp.h b/dll/win32/shell32/precomp.h index f059e5b0b94..f85423ea853 100644 --- a/dll/win32/shell32/precomp.h +++ b/dll/win32/shell32/precomp.h @@ -84,6 +84,7 @@ #include "shellmenu/CMenuSite.h" #include "shellmenu/CMergedFolder.h" #include "shellmenu/shellmenu.h" +#include "CUserNotification.h" #include #include diff --git a/dll/win32/shell32/res/rgs/usernotification.rgs b/dll/win32/shell32/res/rgs/usernotification.rgs new file mode 100644 index 00000000000..61c585f432c --- /dev/null +++ b/dll/win32/shell32/res/rgs/usernotification.rgs @@ -0,0 +1,43 @@ +HKCR +{ + NoRemove AppID + { + {0010890E-8789-413C-ADBC-48F5B511B3AF} + { + val DllSurrogate = s '' + val RunAs = s 'Interactive User' + } + } + NoRemove CLSID + { + ForceRemove {0010890E-8789-413C-ADBC-48F5B511B3AF} = s 'User Notification' + { + InprocServer32 = s '%MODULE%' + { + val ThreadingModel = s 'Apartment' + } + val AppID = s '{0010890E-8789-413C-ADBC-48F5B511B3AF}' + } + NoRemove Interface + { + ForceRemove {7307055C-B24A-486B-9F25-163E597A28A9} = s 'IQueryContinue' + { + NumMethods = s '4' + { + } + ProxyStubClsid32 = s '{B8DA6310-E19B-11D0-933C-00A0C90DCAA9}' + { + } + } + ForceRemove {BA9711BA-5893-4787-A7E1-41277151550B} = s 'IUserNotification' + { + NumMethods = s '8' + { + } + ProxyStubClsid32 = s '{B8DA6310-E19B-11D0-933C-00A0C90DCAA9}' + { + } + } + } + } +} diff --git a/dll/win32/shell32/rgs_res.rc b/dll/win32/shell32/rgs_res.rc index 118eb51e016..67e072d5300 100644 --- a/dll/win32/shell32/rgs_res.rc +++ b/dll/win32/shell32/rgs_res.rc @@ -26,4 +26,5 @@ IDR_DRVDEFEXT REGISTRY "res/rgs/shelldrvdefext.rgs" IDR_EXEDROPHANDLER REGISTRY "res/rgs/exedrophandler.rgs" IDR_MERGEDFOLDER REGISTRY "res/rgs/mergedfolder.rgs" IDR_REBARBANDSITE REGISTRY "res/rgs/rebarbandsite.rgs" +IDR_USERNOTIFICATION REGISTRY "res/rgs/usernotification.rgs" IDR_SHELL REGISTRY "res/rgs/shell.rgs" diff --git a/dll/win32/shell32/shell32.cpp b/dll/win32/shell32/shell32.cpp index 12c5a1e24df..ac0a3bd08c7 100644 --- a/dll/win32/shell32/shell32.cpp +++ b/dll/win32/shell32/shell32.cpp @@ -253,9 +253,10 @@ BEGIN_OBJECT_MAP(ObjectMap) OBJECT_ENTRY(CLSID_MergedFolder, CMergedFolder) OBJECT_ENTRY(CLSID_ExeDropHandler, CExeDropHandler) OBJECT_ENTRY(CLSID_QueryAssociations, CQueryAssociations) + OBJECT_ENTRY(CLSID_UserNotification, CUserNotification) END_OBJECT_MAP() -CShell32Module gModule; +CShell32Module gModule; /*********************************************************************** @@ -311,7 +312,7 @@ STDAPI DllGetVersion(DLLVERSIONINFO *pdvi) * all are once per process * */ -HINSTANCE shell32_hInstance; +HINSTANCE shell32_hInstance; /************************************************************************* * SHELL32 DllMain diff --git a/dll/win32/shell32/shresdef.h b/dll/win32/shell32/shresdef.h index 5b607a69438..53e48967135 100644 --- a/dll/win32/shell32/shresdef.h +++ b/dll/win32/shell32/shresdef.h @@ -689,4 +689,5 @@ #define IDR_QUERYASSOCIATIONS 152 #define IDR_MERGEDFOLDER 153 #define IDR_REBARBANDSITE 154 -#define IDR_SHELL 155 +#define IDR_USERNOTIFICATION 155 +#define IDR_SHELL 156 diff --git a/dll/win32/shell32/systray.cpp b/dll/win32/shell32/systray.cpp index 482abf0e1b2..a64d68950c6 100644 --- a/dll/win32/shell32/systray.cpp +++ b/dll/win32/shell32/systray.cpp @@ -22,7 +22,7 @@ #include "precomp.h" -WINE_DEFAULT_DEBUG_CHANNEL(shell); +WINE_DEFAULT_DEBUG_CHANNEL(shell_notify); /************************************************************************* * Shell_NotifyIcon [SHELL32.296]