From 984b03680fa747697240ce8e6579cf670d551abd Mon Sep 17 00:00:00 2001 From: Frank Praznik Date: Mon, 6 Apr 2026 12:45:56 -0400 Subject: [PATCH] Add the Windows notification driver Notifications are supported on Win10 and above. --- VisualC-GDK/SDL/SDL.vcxproj.filters | 7 +- VisualC/SDL/SDL.vcxproj | 7 +- VisualC/SDL/SDL.vcxproj.filters | 21 + src/core/windows/SDL_windows.c | 5 + src/core/windows/SDL_windows.h | 3 + .../windows/SDL_windowsnotification.c | 1161 +++++++++++++++++ 6 files changed, 1202 insertions(+), 2 deletions(-) create mode 100644 src/notification/windows/SDL_windowsnotification.c diff --git a/VisualC-GDK/SDL/SDL.vcxproj.filters b/VisualC-GDK/SDL/SDL.vcxproj.filters index 9970a0834c..6843eff2ec 100644 --- a/VisualC-GDK/SDL/SDL.vcxproj.filters +++ b/VisualC-GDK/SDL/SDL.vcxproj.filters @@ -4,6 +4,9 @@ + + + @@ -54,6 +57,7 @@ + @@ -241,6 +245,7 @@ + @@ -513,4 +518,4 @@ - + \ No newline at end of file diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj index db8a11f436..643bffef8f 100644 --- a/VisualC/SDL/SDL.vcxproj +++ b/VisualC/SDL/SDL.vcxproj @@ -466,6 +466,7 @@ + @@ -522,6 +523,7 @@ + @@ -624,6 +626,7 @@ + @@ -639,6 +642,8 @@ + + @@ -1000,4 +1005,4 @@ - \ No newline at end of file + diff --git a/VisualC/SDL/SDL.vcxproj.filters b/VisualC/SDL/SDL.vcxproj.filters index 0798f3d750..655fca14fd 100644 --- a/VisualC/SDL/SDL.vcxproj.filters +++ b/VisualC/SDL/SDL.vcxproj.filters @@ -262,6 +262,12 @@ {107d41e6-7c7e-4d9a-a3b3-b6f4abfde0c1} + + {0000b1b2d12877646f6147a5a7510000} + + + {0000b6590eb19c11945581d0479b0000} + @@ -489,6 +495,9 @@ camera + + events + filesystem @@ -501,6 +510,9 @@ main + + notification + @@ -1351,6 +1363,9 @@ dialog + + events + filesystem @@ -1378,6 +1393,12 @@ main\windows + + notification + + + notification\windows + diff --git a/src/core/windows/SDL_windows.c b/src/core/windows/SDL_windows.c index 3f51cbd5fa..4ece1bd46a 100644 --- a/src/core/windows/SDL_windows.c +++ b/src/core/windows/SDL_windows.c @@ -379,6 +379,11 @@ BOOL WIN_IsWindows81OrGreater(void) CHECKWINVER(TRUE, IsWindowsVersionOrGreater(HIBYTE(_WIN32_WINNT_WINBLUE), LOBYTE(_WIN32_WINNT_WINBLUE), 0)); } +BOOL WIN_IsWindows10OrGreater(void) +{ + CHECKWINVER(TRUE, IsWindowsVersionOrGreater(HIBYTE(_WIN32_WINNT_WIN10), LOBYTE(_WIN32_WINNT_WIN10), 0)); +} + BOOL WIN_IsWindows11OrGreater(void) { return IsWindowsBuildVersionAtLeast(22000); diff --git a/src/core/windows/SDL_windows.h b/src/core/windows/SDL_windows.h index e102f200c3..2c18c5cd1b 100644 --- a/src/core/windows/SDL_windows.h +++ b/src/core/windows/SDL_windows.h @@ -196,6 +196,9 @@ extern BOOL WIN_IsWindows8OrGreater(void); // Returns true if we're running on Windows 8.1 and newer extern BOOL WIN_IsWindows81OrGreater(void); +// Returns true if we're running on Windows 10 and newer +extern BOOL WIN_IsWindows10OrGreater(void); + // Returns true if we're running on Windows 11 and newer extern BOOL WIN_IsWindows11OrGreater(void); diff --git a/src/notification/windows/SDL_windowsnotification.c b/src/notification/windows/SDL_windowsnotification.c new file mode 100644 index 0000000000..e3441bcbc0 --- /dev/null +++ b/src/notification/windows/SDL_windowsnotification.c @@ -0,0 +1,1161 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "SDL_internal.h" + +#include "../../events/SDL_notificationevents_c.h" +#include "notification/SDL_notification_c.h" +#include "video/SDL_surface_c.h" + +#include "../../core/windows/SDL_windows.h" +#define COBJMACROS +#include +#include +#include +#include + +typedef HRESULT(WINAPI *RoGetActivationFactory_t)(HSTRING activatableClassId, REFIID iid, void **factory); +typedef HRESULT(WINAPI *RoActivateInstance_t)(HSTRING activatableClassId, IInspectable **instance); +typedef HRESULT(WINAPI *WindowsCreateString_t)(PCWSTR sourceString, UINT32 length, HSTRING *string); +typedef HRESULT(WINAPI *WindowsCreateStringReference_t)(PCWSTR sourceString, UINT32 length, HSTRING_HEADER *hstringHeader, HSTRING *string); +typedef HRESULT(WINAPI *WindowsDeleteString_t)(HSTRING string); +typedef PCWSTR(WINAPI *WindowsGetStringRawBuffer_t)(HSTRING string, UINT32 *length); +typedef NTSTATUS(WINAPI *BCryptGenRandom_t)(BCRYPT_ALG_HANDLE hAlgorithm, PUCHAR pbBuffer, ULONG cbBuffer, ULONG dwFlags); +typedef LONG(WINAPI *GetCurrentPackageFullName_t)(UINT32 *packageFullNameLength, PWSTR packageFullName); + +static RoGetActivationFactory_t WIN_RoGetActivationFactory; +static RoActivateInstance_t WIN_RoActivateInstance; +static WindowsCreateString_t WIN_WindowsCreateString; +static WindowsCreateStringReference_t WIN_WindowsCreateStringReference; +static WindowsDeleteString_t WIN_WindowsDeleteString; +static WindowsGetStringRawBuffer_t WIN_WindowsGetStringRawBuffer; +static GetCurrentPackageFullName_t WIN_GetCurrentPackageFullName; + +// The registry key base needed to register the app instance so notifications can be sent. +#define REG_KEY_BASE L"SOFTWARE\\Classes\\AppUserModelId\\" +#define GROUP_ID_STR L"SDL_LocalNotification" + +// IIDs for the interfaces. +DEFINE_GUID(IID_IToastNotificationManagerStatics, + 0x50ac103f, 0xd235, 0x4598, 0xbb, 0xef, 0x98, 0xfe, 0x4d, 0x1a, 0x3a, 0xd4); + +DEFINE_GUID(IID_IToastNotificationManagerStatics2, + 0x7ab93c52, 0x0e48, 0x4750, 0xba, 0x9d, 0x1a, 0x41, 0x13, 0x98, 0x18, 0x47); + +DEFINE_GUID(IID_IToastNotificationFactory, + 0x04124b20, 0x82c6, 0x4229, 0xb1, 0x09, 0xfd, 0x9e, 0xd4, 0x66, 0x2b, 0x53); + +DEFINE_GUID(IID_IToastNotification2, + 0x9dfb9fd1, 0x143a, 0x490e, 0x90, 0xbf, 0xb9, 0xfb, 0xa7, 0x13, 0x2d, 0xe7); + +DEFINE_GUID(IID_IToastNotification4, + 0x15154935, 0x28ea, 0x4727, 0x88, 0xe9, 0xc5, 0x86, 0x80, 0xe2, 0xd1, 0x18); + +DEFINE_GUID(IID_INotificationActivationCallback, + 0x53e31837, 0x6600, 0x4a81, 0x93, 0x95, 0x75, 0xcf, 0xfe, 0x74, 0x6f, 0x94); + +DEFINE_GUID(IID_IXmlDocument, + 0xf7f3a506, 0x1e87, 0x42d6, 0xbc, 0xfb, 0xb8, 0xc8, 0x09, 0xfa, 0x54, 0x94); + +DEFINE_GUID(IID_IXmlDocumentIO, + 0x6cd0e74e, 0xee65, 0x4489, 0x9e, 0xbf, 0xca, 0x43, 0xe8, 0x7b, 0xa6, 0x37); + +DEFINE_GUID(IID_IToastActivatedEventArgs, + 0xe3bf92f3, 0xc197, 0x436f, 0x82, 0x65, 0x06, 0x25, 0x82, 0x4f, 0x8d, 0xac); + +DEFINE_GUID(IID_IToastActivatedEventHandler, + 0xab54de2d, 0x97d9, 0x5528, 0xb6, 0xad, 0x10, 0x5a, 0xfe, 0x15, 0x65, 0x30); + +DEFINE_GUID(IID_IToastDismissedEventHandler, + 0x61c2402f, 0x0ed0, 0x5a18, 0xab, 0x69, 0x59, 0xf4, 0xaa, 0x99, 0xa3, 0x68); + +static struct Impl_IGeneric *pClassFactory = NULL; + +static HSTRING hsGroupId = NULL; +static HSTRING hsAppId = NULL; + +static __x_ABI_CWindows_CUI_CNotifications_CIToastNotificationManagerStatics *pToastNotificationManager = NULL; +static __x_ABI_CWindows_CUI_CNotifications_CIToastNotifier *pToastNotifier = NULL; +static __x_ABI_CWindows_CUI_CNotifications_CIToastNotificationFactory *pNotificationFactory = NULL; + +static WCHAR *app_reg_key = NULL; +static WCHAR *app_icon_path = NULL; + +static bool ro_initialized = false; +static bool co_initialized = false; + +// IUnknown implementation +typedef struct Impl_IGeneric +{ + IUnknownVtbl *lpVtbl; + SDL_AtomicInt refCount; +} Impl_IGeneric; + +static ULONG STDMETHODCALLTYPE Impl_IGeneric_AddRef(Impl_IGeneric *_this) +{ + return SDL_AddAtomicInt(&_this->refCount, 1) + 1; +} + +// OnActivated interface +static HRESULT STDMETHODCALLTYPE Impl_OnActivated_QueryInterface(__FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_IInspectable *_this, REFIID riid, void **ppvObject) +{ + if (ppvObject == NULL) { + return E_POINTER; + } + if (IsEqualGUID(riid, &IID_IToastActivatedEventHandler) || + IsEqualGUID(riid, &IID_IAgileObject) || + IsEqualGUID(riid, &IID_IUnknown)) { + *ppvObject = _this; + _this->lpVtbl->AddRef(_this); + return S_OK; + } + return E_NOINTERFACE; +} + +static HRESULT STDMETHODCALLTYPE Impl_OnActivated_Invoke(__FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_IInspectable *_this, __x_ABI_CWindows_CUI_CNotifications_CIToastNotification *Sender, IInspectable *Args) +{ + __x_ABI_CWindows_CUI_CNotifications_CIToastActivatedEventArgs *pEventArgs; + HRESULT hr = Args->lpVtbl->QueryInterface(Args, &IID_IToastActivatedEventArgs, (LPVOID *)&pEventArgs); + if (SUCCEEDED(hr)) { + SDL_NotificationID id = 0; + + __x_ABI_CWindows_CUI_CNotifications_CIToastNotification2 *pToastNotification2; + hr = Sender->lpVtbl->QueryInterface(Sender, &IID_IToastNotification2, (LPVOID *)&pToastNotification2); + if (SUCCEEDED(hr)) { + HSTRING hsTag; + hr = pToastNotification2->lpVtbl->get_Tag(pToastNotification2, &hsTag); + if (SUCCEEDED(hr)) { + PCWSTR tag = WIN_WindowsGetStringRawBuffer(hsTag, NULL); + id = (SDL_NotificationID)SDL_wcstoul(tag, NULL, 10); + } + pToastNotification2->lpVtbl->Release(pToastNotification2); + } + + if (id) { + HSTRING hsEventString; + hr = pEventArgs->lpVtbl->get_Arguments(pEventArgs, &hsEventString); + if (SUCCEEDED(hr)) { + PCWCHAR wstr = WIN_WindowsGetStringRawBuffer(hsEventString, NULL); + if (wstr) { + char tmp[512]; + const int len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, tmp, sizeof(tmp), NULL, NULL); + if (len > 0 && len <= sizeof(tmp)) { + SDL_SendNotificationAction(id, tmp); + } + } + } + } + } + + return S_OK; +} + +static __FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_IInspectableVtbl WindowsToast__OnActivatedVtbl = { + .QueryInterface = &Impl_OnActivated_QueryInterface, + .AddRef = (void *)Impl_IGeneric_AddRef, + .Release = (void *)Impl_IGeneric_AddRef, + .Invoke = &Impl_OnActivated_Invoke, +}; + +static __FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_IInspectable OnActivated = { + .lpVtbl = &WindowsToast__OnActivatedVtbl +}; + +// OnDismissed interface +static HRESULT STDMETHODCALLTYPE Impl_OnDismissed_QueryInterface(__FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_Windows__CUI__CNotifications__CToastDismissedEventArgs *_this, REFIID riid, void **ppvObject) +{ + if (ppvObject == NULL) { + return E_POINTER; + } + if (IsEqualGUID(riid, &IID_IToastDismissedEventHandler) || + IsEqualGUID(riid, &IID_IAgileObject) || + IsEqualGUID(riid, &IID_IUnknown)) { + *ppvObject = _this; + _this->lpVtbl->AddRef(_this); + return S_OK; + } + return E_NOINTERFACE; +} + +static HRESULT STDMETHODCALLTYPE Impl_OnDismissed_Invoke(__FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_Windows__CUI__CNotifications__CToastDismissedEventArgs *_this, __x_ABI_CWindows_CUI_CNotifications_CIToastNotification *Sender, __x_ABI_CWindows_CUI_CNotifications_CIToastDismissedEventArgs *Args) +{ + __x_ABI_CWindows_CUI_CNotifications_CToastDismissalReason Reason; + Args->lpVtbl->get_Reason(Args, &Reason); + + /* Remove transient notifications that were cancelled or timed out, + * so they won't persist in the notification center. + */ + switch (Reason) { + case ToastDismissalReason_TimedOut: + case ToastDismissalReason_UserCanceled: + pToastNotifier->lpVtbl->Hide(pToastNotifier, Sender); + break; + + default: + break; + } + + return S_OK; +} + +static __FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_Windows__CUI__CNotifications__CToastDismissedEventArgsVtbl WindowsToast__OnDismissedVtbl = { + .QueryInterface = &Impl_OnDismissed_QueryInterface, + .AddRef = (void *)Impl_IGeneric_AddRef, + .Release = (void *)Impl_IGeneric_AddRef, + .Invoke = &Impl_OnDismissed_Invoke, +}; + +static __FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_Windows__CUI__CNotifications__CToastDismissedEventArgs OnDismissed = { + .lpVtbl = &WindowsToast__OnDismissedVtbl +}; + +static bool IsInPackage() +{ + if (!WIN_GetCurrentPackageFullName) { + HMODULE kernel32 = GetModuleHandle(TEXT("kernel32.dll")); + WIN_GetCurrentPackageFullName = (GetCurrentPackageFullName_t)GetProcAddress(kernel32, "GetCurrentPackageFullName"); + } + + if (WIN_GetCurrentPackageFullName) { + UINT32 length = 0; + LONG rc = WIN_GetCurrentPackageFullName(&length, NULL); + if (rc != ERROR_INSUFFICIENT_BUFFER) { + if (rc == APPMODEL_ERROR_NO_PACKAGE) { + return false; + } else { + return true; + } + } + } + + return false; +} + +static WCHAR *GetExePath() +{ + DWORD buflen = MAX_PATH; + WCHAR *path = NULL; + DWORD len = 0; + + for (;;) { + WCHAR *ptr = SDL_realloc(path, buflen * sizeof(WCHAR)); + if (!ptr) { + SDL_free(path); + return NULL; + } + + path = ptr; + + len = GetModuleFileNameW(NULL, path, buflen); + // If this was truncated, then len >= buflen - 1 + if (len < buflen - 1) { + break; + } + + // buffer too small? Try again. + buflen *= 2; + } + + if (len == 0) { + SDL_free(path); + return NULL; + } + + return path; +} + +/* There is no way to pass an image as a byte stream when creating Windows + * notifications, so surfaces are saved as PNG files to temporary storage + * while the notification is being shown, then cleaned up a few seconds later. + */ +typedef struct ToastIcon +{ + struct ToastIcon *next; + WCHAR icon_file[1]; +} ToastIcon; + +static ToastIcon *toast_icons = NULL; +static UINT_PTR cleanup_timer_id = 0; + +static void CleanupIcons() +{ + KillTimer(NULL, cleanup_timer_id); + cleanup_timer_id = 0; + + for (ToastIcon *i = toast_icons; i; i = toast_icons) { + DeleteFileW(i->icon_file); + toast_icons = i->next; + SDL_free(i); + } +} + +static void CALLBACK IconCleanupCallback(HWND hWnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) +{ + CleanupIcons(); +} + +static WCHAR *GetFullPath(const char *path) +{ + const size_t conv_len = SDL_strlen(path) + 1; + + WCHAR *conv_path = SDL_malloc(conv_len * sizeof(WCHAR)); + if (!conv_path) { + return NULL; + } + MultiByteToWideChar(CP_UTF8, 0, path, -1, conv_path, (int)conv_len); + + const DWORD full_len = GetFullPathNameW(conv_path, 0, NULL, NULL); + if (!full_len) { + SDL_free(conv_path); + return NULL; + } + + WCHAR *full_path = SDL_malloc(full_len * sizeof(WCHAR)); + if (!full_path) { + SDL_free(conv_path); + return NULL; + } + GetFullPathNameW(conv_path, full_len, full_path, NULL); + SDL_free(conv_path); + + // Make sure the file actually exists. + WIN32_FILE_ATTRIBUTE_DATA attr; + if (GetFileAttributesExW(full_path, GetFileExInfoStandard, &attr)) { + return full_path; + } + + SDL_free(full_path); + return NULL; +} + +static const WCHAR *GetToastIconPath() +{ + if (app_icon_path) { + return app_icon_path; + } + + const char *icon = SDL_GetStringProperty(SDL_GetGlobalProperties(), SDL_PROP_GLOBAL_NOTIFICATION_HEADER_ICON_STRING, NULL); + if (icon) { + app_icon_path = GetFullPath(icon); + } + + return app_icon_path; +} + +static WCHAR *SaveToastImage(SDL_Surface *icon) +{ + if (icon) { + // The documentation states that the buffers for these should be MAX_PATH. + WCHAR *temp_path = NULL; + size_t path_len = 0; + + // Stop any timers so they won't fire in the middle of this. + if (cleanup_timer_id) { + KillTimer(NULL, cleanup_timer_id); + } + + path_len = GetTempPath2W(0, NULL); + if (!path_len) { + return NULL; + } + temp_path = SDL_realloc(temp_path, (path_len + 1) * sizeof(WCHAR)); + path_len = GetTempPath2W((DWORD)path_len + 1, temp_path); + if (!path_len) { + SDL_free(temp_path); + return NULL; + } + + WCHAR file_name[MAX_PATH]; + const UINT name_ret = GetTempFileNameW(temp_path, L"SDL", 0, file_name); + SDL_free(temp_path); + if (!name_ret) { + return NULL; + } + + path_len += SDL_wcslen(file_name) + 5; + + WCHAR *path_buf = NULL; + ToastIcon *toast_icon = SDL_calloc(1, sizeof(ToastIcon) + (path_len * sizeof(WCHAR))); + toast_icon->next = toast_icons; + toast_icons = toast_icon; + path_buf = toast_icon->icon_file; + + SDL_wcslcat(path_buf, file_name, path_len); + SDL_wcslcat(path_buf, L".png", path_len); + + const int len = WideCharToMultiByte(CP_UTF8, 0, path_buf, -1, NULL, 0, NULL, NULL); + char *png_path = SDL_malloc(len * sizeof(char)); + WideCharToMultiByte(CP_UTF8, 0, path_buf, -1, png_path, len, NULL, NULL); + SDL_SavePNG(icon, png_path); + SDL_free(png_path); + + // Duplicate the path, since the source object will be destroyed by a timer. + path_buf = SDL_wcsdup(path_buf); + + // Schedule a cleanup of icons 5 seconds from now. + cleanup_timer_id = SetTimer(NULL, 0, 5000, IconCleanupCallback); + + return path_buf; + } + + return NULL; +} + +static WCHAR *GetAppMetadata(const char *metadata_name) +{ + WCHAR *metadata = NULL; + int id_len = 0; + + const char *app_metadata = SDL_GetAppMetadataProperty(metadata_name); + if (app_metadata && *app_metadata != '\0') { + id_len = MultiByteToWideChar(CP_UTF8, 0, app_metadata, -1, NULL, 0); + if (id_len > 0) { + metadata = SDL_malloc(id_len * sizeof(WCHAR)); + if (!metadata) { + return NULL; + } + + MultiByteToWideChar(CP_UTF8, 0, app_metadata, -1, metadata, id_len); + return metadata; + } + } else { + WCHAR *wszExePath = GetExePath(); + + for (WCHAR *c = wszExePath + SDL_wcslen(wszExePath); c >= wszExePath; --c) { + if (*c == L'/' || *c == L'\\') { + metadata = c + 1; + break; + } + } + + if (!metadata) { + metadata = wszExePath; + } + + metadata = SDL_wcsdup(metadata); + SDL_free(wszExePath); + + return metadata; + } + + return NULL; +} + +static bool InitToastSystem() +{ + static bool initialized = false; + + if (initialized) { + return true; + } + +#define RESOLVE(x) \ + WIN_##x = (x##_t)WIN_LoadComBaseFunction(#x); \ + if (!WIN_##x) \ + return WIN_SetError("GetProcAddress failed for " #x) + RESOLVE(RoGetActivationFactory); + RESOLVE(RoActivateInstance); + RESOLVE(WindowsCreateString); + RESOLVE(WindowsCreateStringReference); + RESOLVE(WindowsGetStringRawBuffer); + RESOLVE(WindowsDeleteString); +#undef RESOLVE + + HSTRING_HEADER hshToastNotificationManager; + HSTRING hsToastNotificationManager = NULL; + HSTRING_HEADER hshToastNotification; + HSTRING hsToastNotification = NULL; + WCHAR *image_path = NULL; + + // Initialize COM and Windows runtime with the same threading model. + HRESULT hr = WIN_CoInitialize(); + if (FAILED(hr)) { + return false; + } + co_initialized = true; + + hr = WIN_RoInitialize(); + if (FAILED(hr)) { + return false; + } + ro_initialized = true; + + // Get the application ID and name. + WCHAR *app_id = GetAppMetadata(SDL_PROP_APP_METADATA_IDENTIFIER_STRING); + WCHAR *app_name = GetAppMetadata(SDL_PROP_APP_METADATA_NAME_STRING); + + // Create the persistent appID string. + hr = WIN_WindowsCreateString(app_id, (UINT32)SDL_wcslen(app_id), &hsAppId); + if (FAILED(hr)) { + goto cleanup; + } + + // Create the persistent groupID string. + hr = WIN_WindowsCreateString(GROUP_ID_STR, (UINT32)SDL_wcslen(GROUP_ID_STR), &hsGroupId); + if (FAILED(hr)) { + goto cleanup; + } + + // Build the registry key. + { + size_t reg_key_len = SDL_wcslen(REG_KEY_BASE) + SDL_wcslen(app_id) + 1; + app_reg_key = SDL_malloc(reg_key_len * sizeof(WCHAR)); + if (!app_reg_key) { + goto cleanup; + } + SDL_swprintf(app_reg_key, reg_key_len, L"%ls%ls", REG_KEY_BASE, app_id); + } + + // Set the app name and icon. + { + const WCHAR *icon_path = GetToastIconPath(); + if (icon_path) { + hr = HRESULT_FROM_WIN32(RegSetKeyValueW(HKEY_CURRENT_USER, app_reg_key, L"IconUri", REG_SZ, icon_path, (DWORD)(SDL_wcslen(icon_path) * sizeof(WCHAR)))); + if (FAILED(hr)) { + goto cleanup; + } + } else { + // This will "fail" if the key already doesn't exist, which is fine. + RegDeleteKeyValueW(HKEY_CURRENT_USER, app_reg_key, L"IconUri"); + } + } + + hr = HRESULT_FROM_WIN32(RegSetKeyValueW(HKEY_CURRENT_USER, app_reg_key, L"DisplayName", REG_SZ, app_name, (DWORD)(SDL_wcslen(app_name) * sizeof(WCHAR)))); + if (FAILED(hr)) { + goto cleanup; + } + + hr = WIN_WindowsCreateStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager, sizeof(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager) / sizeof(WCHAR) - 1, &hshToastNotificationManager, &hsToastNotificationManager); + if (FAILED(hr)) { + goto cleanup; + } + + hr = WIN_RoGetActivationFactory(hsToastNotificationManager, &IID_IToastNotificationManagerStatics, (LPVOID *)&pToastNotificationManager); + if (FAILED(hr)) { + goto cleanup; + } + + hr = pToastNotificationManager->lpVtbl->CreateToastNotifierWithId(pToastNotificationManager, hsAppId, &pToastNotifier); + if (FAILED(hr)) { + goto cleanup; + } + + hr = WIN_WindowsCreateStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification, (UINT32)(sizeof(RuntimeClass_Windows_UI_Notifications_ToastNotification) / sizeof(wchar_t) - 1), &hshToastNotification, &hsToastNotification); + if (FAILED(hr)) { + goto cleanup; + } + + hr = WIN_RoGetActivationFactory(hsToastNotification, &IID_IToastNotificationFactory, (LPVOID *)&pNotificationFactory); + if (FAILED(hr)) { + goto cleanup; + } + + initialized = true; + +cleanup: + WIN_WindowsDeleteString(hsToastNotificationManager); + WIN_WindowsDeleteString(hsToastNotification); + SDL_free(image_path); + SDL_free(app_id); + SDL_free(app_name); + + return initialized; +} + +static bool AppendXmlAudio(SDL_IOStream *dst, const char *sound) +{ + static const WCHAR *default_sound_path = L"ms-winsoundevent:Notification.Default"; + + WCHAR buf[512]; + WCHAR *buf_ptr = buf; + WCHAR *path_format = NULL; + const WCHAR *silent_str = L"false"; + const WCHAR *sound_path = default_sound_path; + int buf_len = SDL_arraysize(buf); + + if (SDL_strcmp(sound, "silent") == 0) { + silent_str = L"true"; + } else if (SDL_strcmp(sound, "default") != 0) { + /* Windows currently only loads custom notification sounds when the app is + * in an MSIX package. We'll prepend a 'file:///' prefix when not in a package + * in case this changes in the future, but for now, it just plays the default + * sound. + */ + if (IsInPackage()) { + const WCHAR *prefix = L"ms-appx:///"; + const size_t prefix_len = 11; + + const size_t path_len = SDL_strlen(sound) + prefix_len + 1; + if (path_len) { + path_format = SDL_malloc(sizeof(WCHAR *) * path_len); + if (!path_format) { + return false; + } + SDL_wcslcpy(path_format, prefix, path_len); + + if (*sound == '/') { + ++sound; + } + MultiByteToWideChar(CP_UTF8, 0, sound, -1, path_format + prefix_len, (int)(path_len - prefix_len)); + sound_path = path_format; + } + } else { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Windows does not support custom notification sounds outside an MSIX package"); + + WCHAR *path = GetFullPath(sound); + if (path) { + const WCHAR *prefix = L"file:///"; + const DWORD prefix_len = 8; + + const size_t path_len = SDL_wcslen(path) + prefix_len + 1; + sound_path = path_format = SDL_malloc(sizeof(WCHAR *) * path_len); + if (!path_format) { + SDL_free(path); + return false; + } + + SDL_wcslcpy(path_format, prefix, path_len); + SDL_wcslcat(path_format, path, path_len); + SDL_free(path); + } + } + } + + for (;;) { + const int len = SDL_swprintf(buf_ptr, buf_len, L"