| /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ |
| /* GIO - GLib Input, Output and Streaming Library |
| * |
| * Copyright © 2018 Endless Mobile, Inc. |
| * Copyright © 2025 Oscar Pernia <oscarperniamoreno@gmail.com> |
| * |
| * SPDX-License-Identifier: LGPL-2.1-or-later |
| * |
| * 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, see |
| * <http://www.gnu.org/licenses/>. |
| * |
| * Authors: |
| * - Philip Withnall <withnall@endlessm.com> |
| * - Oscar Pernia <oscarperniamoreno@gmail.com> - Initial implementation using legacy Shell_NotifyIcon API |
| */ |
| |
| #include "config.h" |
| |
| #include <windows.h> |
| |
| #include "gnotificationbackend.h" |
| |
| #include "giomodule-priv.h" |
| #include "glib-private.h" |
| #include "gnotification-private.h" |
| |
| #define G_TYPE_WIN32_NOTIFICATION_BACKEND (g_win32_notification_backend_get_type ()) |
| #define G_WIN32_NOTIFICATION_BACKEND(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), G_TYPE_WIN32_NOTIFICATION_BACKEND, GWin32NotificationBackend)) |
| |
| extern IMAGE_DOS_HEADER __ImageBase; |
| |
| static inline HMODULE |
| exe_module (void) |
| { |
| return (HMODULE) GetModuleHandle (NULL); |
| } |
| |
| static inline HMODULE |
| this_module (void) |
| { |
| return (HMODULE) &__ImageBase; |
| } |
| |
| static HWND hwnd; |
| static GMutex hwnd_mutex; /* Protects access to `hwnd`, `hwnd_refcount`, `hwnd_state` and `wnd_klass` */ |
| static grefcount hwnd_refcount; |
| static ATOM wnd_klass; |
| |
| typedef enum |
| { |
| HWND_STATE_READY, |
| HWND_STATE_FAILED, |
| HWND_STATE_UNINITIALIZED, |
| HWND_STATE_INITIALIZING, |
| HWND_STATE_DESTROYING, |
| |
| /* Should set this state if Shell_NotifyIcon(NIM_MODIFY (or NIM_ADD on init method), ...) |
| * failed. If this is the state set, the calling thread should wait on `hwnd_cond` |
| * for `hwnd_state` to change. |
| * |
| * Should go back to READY after dummy_WndProc got TaskBarCreated message and succesfully called |
| * Shell_NotifyIcon(NIM_ADD, ...), if that call also failed, the state changes to FAILED instead. */ |
| HWND_STATE_INITIALIZING_NOTIFY_ICON, |
| } HwndState; |
| |
| /* Specifies the state in which `hwnd` initialization is in. */ |
| static HwndState hwnd_state = HWND_STATE_UNINITIALIZED; |
| |
| /* Condition variable for changes in `hwnd_state`. */ |
| static GCond hwnd_cond; |
| |
| typedef struct _GWin32NotificationBackend GWin32NotificationBackend; |
| typedef GNotificationBackendClass GWin32NotificationBackendClass; |
| |
| struct _GWin32NotificationBackend |
| { |
| GNotificationBackend parent; |
| |
| /* Prevents double-frees when disposing of the backend, if it's non-NULL, |
| * we decrement `hwnd_refcount` and set this to NULL */ |
| HWND hwnd; |
| }; |
| |
| GType g_win32_notification_backend_get_type (void); |
| |
| G_DEFINE_TYPE_WITH_CODE (GWin32NotificationBackend, g_win32_notification_backend, G_TYPE_NOTIFICATION_BACKEND, |
| _g_io_modules_ensure_extension_points_registered (); |
| g_io_extension_point_implement (G_NOTIFICATION_BACKEND_EXTENSION_POINT_NAME, |
| g_define_type_id, "win32", 0)) |
| |
| /* Maximum number of UTF-16 code units that can be written into |
| * NOTIFYICONDATA.szInfoTitle array, it does not count one element |
| * which is for the NUL-terminator */ |
| #define MAX_TITLE_COUNT (G_N_ELEMENTS (((NOTIFYICONDATA *) 0)->szInfoTitle) - 1) |
| |
| /* Maximum number of UTF-16 code units that can be written into |
| * NOTIFYICONDATA.szInfo array, it does not count one element |
| * which is for the NUL-terminator */ |
| #define MAX_BODY_COUNT (G_N_ELEMENTS (((NOTIFYICONDATA *) 0)->szInfo) - 1) |
| |
| /* Initializes `out` with a NOTIFYICONDATA struct, to be passed to |
| * Shell_NotifyIcon (NIM_ADD, ...) calls. */ |
| #define G_NOTIFYICONDATA_INIT(out) \ |
| G_STMT_START \ |
| { \ |
| *(out) = (NOTIFYICONDATA){ \ |
| .cbSize = sizeof (NOTIFYICONDATA), \ |
| .hWnd = hwnd, \ |
| .uFlags = NIF_ICON, \ |
| .hIcon = LoadIcon (exe_module (), MAKEINTRESOURCE (1)), \ |
| }; \ |
| \ |
| if (!(out)->hIcon) \ |
| { \ |
| /* Fallback if the application has no icon */ \ |
| (out)->hIcon = LoadIcon (NULL, IDI_APPLICATION); \ |
| } \ |
| } \ |
| G_STMT_END |
| |
| static gboolean |
| g_win32_notification_backend_is_supported (void) |
| { |
| /* This backend is always supported on Windows. */ |
| return TRUE; |
| } |
| |
| /* Implementing send-and-forget notifications, only supporting W10/11 */ |
| static void |
| g_win32_notification_backend_send_notification (GNotificationBackend *backend, |
| const gchar *id, |
| GNotification *notification) |
| { |
| /* GIO users may expect notification actions to work, and expect the actions |
| * to be activated, but because we cannot fullfil the request, it's appropriate |
| * to warn about it at least once to let them know. */ |
| |
| if (g_notification_get_n_buttons (notification) > 0 || |
| g_notification_get_default_action (notification, NULL, NULL)) |
| { |
| g_warning_once ("Notification actions are unsupported by this Windows backend"); |
| } |
| |
| /* NOTE: Icon is unsupported for W10 or greater, trying to set an icon will |
| * make the notification to not show up */ |
| /* NOTE: There is no category */ |
| /* NOTE: There is no priority */ |
| |
| GWin32NotificationBackend *self = G_WIN32_NOTIFICATION_BACKEND (backend); |
| |
| g_mutex_lock (&hwnd_mutex); |
| |
| /* Return early if backend's initialization failed */ |
| if (hwnd_state != HWND_STATE_READY && |
| hwnd_state != HWND_STATE_INITIALIZING_NOTIFY_ICON) |
| { |
| goto err_out; |
| } |
| |
| NOTIFYICONDATA notify_singleton = { |
| .cbSize = sizeof (NOTIFYICONDATA), |
| .hWnd = self->hwnd, |
| .uFlags = NIF_INFO, |
| }; |
| |
| GError *error = NULL; |
| glong items_written; |
| |
| const gchar *title_utf8 = g_notification_get_title (notification); |
| WCHAR *title_utf16 = g_utf8_to_utf16 (title_utf8, -1, NULL, &items_written, &error); |
| if (error) |
| { |
| g_critical ("Invalid UTF-8 in notification: %s", error->message); |
| g_error_free (error); |
| goto err_out; /* Cannot show a notification without a title */ |
| } |
| |
| g_assert (items_written >= 0); |
| if ((size_t) items_written > MAX_TITLE_COUNT) |
| { |
| g_warning ("Notification title too long, truncating title"); |
| items_written = MAX_TITLE_COUNT; |
| if (IS_LOW_SURROGATE (title_utf16[items_written])) |
| items_written--; |
| } |
| |
| if (items_written == 0) |
| { |
| g_critical ("Notification title is empty"); |
| g_free (title_utf16); |
| goto err_out; /* Cannot show a notification without a title */ |
| } |
| |
| memcpy (¬ify_singleton.szInfoTitle, title_utf16, items_written * sizeof (WCHAR)); |
| notify_singleton.szInfoTitle[items_written] = L'\0'; |
| g_free (title_utf16); |
| |
| const gchar *body_utf8 = g_notification_get_body (notification); |
| if (!body_utf8 || !*body_utf8) |
| { |
| /* If you pass an empty body, Shell_NotifyIcon won't show the notification, |
| * in order to fix this, here we are setting a string with a single |
| * SPACE (' ') character, which makes the body look like empty. |
| * |
| * > To remove the balloon notification from the UI (...) |
| * > set the `NIF_INFO` flag in `uFlags` and set `szInfo` to an empty string. |
| * |
| * https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-notifyicondataw */ |
| notify_singleton.szInfo[0] = L' '; |
| notify_singleton.szInfo[1] = L'\0'; |
| } |
| else |
| { |
| WCHAR *body_utf16 = g_utf8_to_utf16 (body_utf8, -1, NULL, &items_written, &error); |
| if (error) |
| { |
| g_critical ("Invalid UTF-8 in notification: %s", error->message); |
| g_error_free (error); |
| goto err_out; /* Cannot show a notification without a body */ |
| } |
| |
| g_assert (items_written >= 0); |
| if ((size_t) items_written > MAX_BODY_COUNT) |
| { |
| g_warning ("Notification body too long, truncating body"); |
| items_written = MAX_BODY_COUNT; |
| if (IS_LOW_SURROGATE (body_utf16[items_written])) |
| items_written--; |
| } |
| |
| memcpy (¬ify_singleton.szInfo, body_utf16, items_written * sizeof (WCHAR)); |
| notify_singleton.szInfo[items_written] = L'\0'; |
| g_free (body_utf16); |
| } |
| |
| BOOL ret; |
| |
| /* Loops until Shell_NotifyIcon call reports success, the worker thread |
| * failed to create the notification icon or it timed out waiting for it. */ |
| do |
| { |
| gint64 end_time = g_get_monotonic_time () + 5 * G_TIME_SPAN_SECOND; |
| |
| /* Wait until notification icon is initialized */ |
| while (hwnd_state == HWND_STATE_INITIALIZING_NOTIFY_ICON) |
| if (!g_cond_wait_until (&hwnd_cond, &hwnd_mutex, end_time)) |
| { |
| /* Timeout has passed */ |
| g_debug ("Timeout in send_notification while waiting for the notification icon to be created"); |
| goto err_out; |
| } |
| |
| if (hwnd_state == HWND_STATE_FAILED) |
| { |
| goto err_out; |
| } |
| |
| if (!(ret = Shell_NotifyIcon (NIM_MODIFY, ¬ify_singleton))) |
| { |
| /* Assume shell's task bar is still not ready, set INITIALIZING_NOTIFY_ICON state |
| * and wait until dummy_WndProc receives TaskBarCreated message */ |
| hwnd_state = HWND_STATE_INITIALIZING_NOTIFY_ICON; |
| } |
| } |
| while (!ret); |
| |
| err_out: |
| g_mutex_unlock (&hwnd_mutex); |
| } |
| |
| static void |
| g_win32_notification_backend_withdraw_notification (GNotificationBackend *backend, |
| const gchar *id) |
| { |
| } |
| |
| /* Runs on GLib worker thread */ |
| static gboolean |
| destroy_window_worker (void) |
| { |
| g_mutex_lock (&hwnd_mutex); |
| |
| g_assert (hwnd_state == HWND_STATE_DESTROYING); |
| |
| DestroyWindow (hwnd); |
| hwnd = NULL; |
| UnregisterClass (MAKEINTATOM (wnd_klass), this_module ()); |
| wnd_klass = 0; |
| hwnd_state = HWND_STATE_UNINITIALIZED; |
| |
| g_cond_broadcast (&hwnd_cond); |
| g_mutex_unlock (&hwnd_mutex); |
| |
| return G_SOURCE_REMOVE; |
| } |
| |
| static void |
| g_win32_notification_backend_dispose (GObject *self) |
| { |
| GWin32NotificationBackend *backend = G_WIN32_NOTIFICATION_BACKEND (self); |
| |
| g_mutex_lock (&hwnd_mutex); |
| |
| if (backend->hwnd && g_ref_count_dec (&hwnd_refcount)) |
| { |
| /* Window must be destroyed by the same thread that created it. |
| * |
| * https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-destroywindow#remarks */ |
| |
| hwnd_state = HWND_STATE_DESTROYING; |
| |
| g_main_context_invoke (GLIB_PRIVATE_CALL (g_get_worker_context) (), |
| G_SOURCE_FUNC (destroy_window_worker), NULL); |
| |
| /* NOTE: threads that call *_init method wait for DESTROYING state to change to UNINITIALIZED, |
| * there is no need to do that here. */ |
| } |
| |
| backend->hwnd = NULL; |
| |
| g_mutex_unlock (&hwnd_mutex); |
| |
| G_OBJECT_CLASS (g_win32_notification_backend_parent_class)->dispose (self); |
| } |
| |
| /* Not dummy anymore... */ |
| static LRESULT CALLBACK |
| dummy_WndProc (HWND _hwnd, UINT message, WPARAM wparam, LPARAM lparam) |
| { |
| /* FIXME: We could add some nice features: |
| * |
| * - If we got notification icon click event, activate the application. */ |
| |
| static UINT msg_TaskbarCreated; |
| |
| switch (message) |
| { |
| case WM_CREATE: |
| { |
| msg_TaskbarCreated = RegisterWindowMessage (L"TaskbarCreated"); |
| if (msg_TaskbarCreated == 0) |
| { |
| g_warning ("win32-notification: RegisterWindowMessage failed: '%ld'", GetLastError ()); |
| } |
| break; |
| } |
| case WM_NULL: |
| { |
| break; |
| } |
| default: |
| { |
| if (message == msg_TaskbarCreated) |
| { |
| g_mutex_lock (&hwnd_mutex); |
| |
| if (hwnd_state != HWND_STATE_READY && |
| hwnd_state != HWND_STATE_INITIALIZING_NOTIFY_ICON) |
| { |
| g_mutex_unlock (&hwnd_mutex); |
| break; |
| } |
| |
| NOTIFYICONDATA notify_singleton; |
| G_NOTIFYICONDATA_INIT (¬ify_singleton); |
| |
| if (!Shell_NotifyIcon (NIM_ADD, ¬ify_singleton)) |
| { |
| hwnd_state = HWND_STATE_FAILED; |
| } |
| else |
| { |
| hwnd_state = HWND_STATE_READY; |
| Shell_NotifyIcon (NIM_SETVERSION, ¬ify_singleton); |
| } |
| |
| g_cond_broadcast (&hwnd_cond); |
| g_mutex_unlock (&hwnd_mutex); |
| } |
| } |
| } |
| |
| return DefWindowProc (_hwnd, message, wparam, lparam); |
| } |
| |
| /* Runs on GLib worker thread */ |
| static gboolean |
| create_window_worker (void) |
| { |
| g_mutex_lock (&hwnd_mutex); |
| |
| g_assert (hwnd_state == HWND_STATE_INITIALIZING); |
| |
| WNDCLASS wclass = { |
| .lpszClassName = L"GWin32NotificationBackend", |
| .hInstance = this_module (), |
| .lpfnWndProc = dummy_WndProc, |
| }; |
| |
| wnd_klass = RegisterClass (&wclass); |
| if (!wnd_klass) |
| { |
| g_critical ("win32-notification: RegisterClass failed: %ld", GetLastError ()); |
| hwnd_state = HWND_STATE_FAILED; |
| goto err_out; |
| } |
| |
| hwnd = CreateWindow (MAKEINTATOM (wnd_klass), NULL, WS_POPUP, |
| 0, 0, 0, 0, NULL, NULL, this_module (), NULL); |
| |
| if (!hwnd) |
| { |
| g_critical ("win32-notification: CreateWindow failed: %ld", GetLastError ()); |
| UnregisterClass (MAKEINTATOM (wnd_klass), this_module ()); |
| wnd_klass = 0; |
| hwnd_state = HWND_STATE_FAILED; |
| goto err_out; |
| } |
| |
| g_ref_count_init (&hwnd_refcount); |
| |
| /* Create the notification icon for the first time */ |
| |
| NOTIFYICONDATA notify_singleton; |
| G_NOTIFYICONDATA_INIT (¬ify_singleton); |
| |
| if (!Shell_NotifyIcon (NIM_ADD, ¬ify_singleton)) |
| { |
| /* Assume shell's task bar is still not ready, set INITIALIZING_NOTIFY_ICON state |
| * and wait until dummy_WndProc receives TaskbarCreated message */ |
| hwnd_state = HWND_STATE_INITIALIZING_NOTIFY_ICON; |
| } |
| else |
| { |
| Shell_NotifyIcon (NIM_SETVERSION, ¬ify_singleton); |
| hwnd_state = HWND_STATE_READY; |
| } |
| |
| err_out: |
| g_cond_broadcast (&hwnd_cond); |
| g_mutex_unlock (&hwnd_mutex); |
| |
| return G_SOURCE_REMOVE; |
| } |
| |
| /* {{{ GWin32MessageSource |
| * |
| * Copied from https://gitlab.gnome.org/GNOME/gtk/-/blob/main/gdk/win32/gdkwin32messagesource.c |
| * |
| * FIXME: Move all of this code to centralized module. */ |
| |
| typedef struct |
| { |
| GSource source; |
| GPollFD poll_fd; |
| } GWin32MessageSource; |
| |
| /* Runs on GLib worker thread */ |
| static gboolean |
| g_win32_message_source_prepare (GSource *source, |
| int *timeout) |
| { |
| *timeout = -1; |
| |
| return GetQueueStatus (QS_ALLINPUT) != 0; |
| } |
| |
| /* Runs on GLib worker thread */ |
| static gboolean |
| g_win32_message_source_check (GSource *source) |
| { |
| GWin32MessageSource *self = (GWin32MessageSource *) source; |
| |
| self->poll_fd.revents = 0; |
| |
| return GetQueueStatus (QS_ALLINPUT) != 0; |
| } |
| |
| /* Runs on GLib worker thread */ |
| static gboolean |
| g_win32_message_source_dispatch (GSource *source, |
| GSourceFunc callback, |
| gpointer user_data) |
| { |
| MSG msg; |
| |
| /* Message loop with upper limit, prevents GSource loop starvation and |
| * allows to process big batches of messages in a single dispatch. */ |
| for (int i = 0; i < 100 && PeekMessage (&msg, NULL, 0, 0, PM_REMOVE); i++) |
| { |
| TranslateMessage (&msg); |
| DispatchMessage (&msg); |
| } |
| |
| return G_SOURCE_CONTINUE; |
| } |
| |
| static void |
| g_win32_message_source_ensure_running (void) |
| { |
| static gsize message_source_initialized = 0; |
| |
| if (g_once_init_enter (&message_source_initialized)) |
| { |
| static GSourceFuncs source_funcs = { |
| .prepare = g_win32_message_source_prepare, |
| .check = g_win32_message_source_check, |
| .dispatch = g_win32_message_source_dispatch, |
| /* No finalize, the worker thread lasts until the end of the program, |
| * so the worker's GMainContext isn't going to be unref-ed. |
| * We don't return G_SOURCE_REMOVE from dispatch as well. */ |
| .finalize = NULL, |
| }; |
| |
| GSource *source = g_source_new (&source_funcs, sizeof (GWin32MessageSource)); |
| GWin32MessageSource *message_source = (GWin32MessageSource *) source; |
| |
| g_source_set_static_name (source, "GLib Win32 worker message source"); |
| g_source_set_priority (source, G_PRIORITY_DEFAULT); |
| g_source_set_can_recurse (source, TRUE); |
| |
| message_source->poll_fd.events = G_IO_IN; |
| #if defined(G_WITH_CYGWIN) |
| message_source->poll_fd.fd = open ("/dev/windows", O_RDONLY); |
| if (message_source->poll_fd.fd == -1) |
| g_error ("can't open \"/dev/windows\": %s", g_strerror (errno)); |
| #else |
| message_source->poll_fd.fd = G_WIN32_MSG_HANDLE; |
| #endif |
| |
| g_source_add_poll (source, &message_source->poll_fd); |
| g_source_attach (source, GLIB_PRIVATE_CALL (g_get_worker_context) ()); |
| g_source_unref (source); |
| |
| g_once_init_leave (&message_source_initialized, 1); |
| } |
| } |
| |
| /* }}} */ |
| |
| static void |
| g_win32_notification_backend_init (GWin32NotificationBackend *backend) |
| { |
| /* FIXME: Move this to centralized module */ |
| g_win32_message_source_ensure_running (); |
| |
| /* Don't increment on UNINITIALIZED or if `hwnd` is NULL, at every other case, |
| * it should increment. */ |
| gboolean needs_inc = TRUE; |
| |
| g_mutex_lock (&hwnd_mutex); |
| |
| while (hwnd_state == HWND_STATE_DESTROYING) |
| g_cond_wait (&hwnd_cond, &hwnd_mutex); |
| |
| if (hwnd_state == HWND_STATE_UNINITIALIZED) |
| { |
| hwnd_state = HWND_STATE_INITIALIZING; |
| needs_inc = FALSE; /* Alredy incremented in worker */ |
| g_main_context_invoke (GLIB_PRIVATE_CALL (g_get_worker_context) (), |
| G_SOURCE_FUNC (create_window_worker), NULL); |
| } |
| |
| while (hwnd_state == HWND_STATE_INITIALIZING) |
| g_cond_wait (&hwnd_cond, &hwnd_mutex); |
| |
| if (hwnd) |
| { |
| backend->hwnd = hwnd; |
| |
| if (needs_inc) |
| g_ref_count_inc (&hwnd_refcount); |
| } |
| |
| g_mutex_unlock (&hwnd_mutex); |
| } |
| |
| static void |
| g_win32_notification_backend_class_init (GWin32NotificationBackendClass *class) |
| { |
| GObjectClass *object_class = G_OBJECT_CLASS (class); |
| GNotificationBackendClass *backend_class = G_NOTIFICATION_BACKEND_CLASS (class); |
| |
| object_class->dispose = g_win32_notification_backend_dispose; |
| |
| backend_class->is_supported = g_win32_notification_backend_is_supported; |
| backend_class->send_notification = g_win32_notification_backend_send_notification; |
| backend_class->withdraw_notification = g_win32_notification_backend_withdraw_notification; |
| } |