blob: 160983e1b8a1da5df75a85e028c1de46c8f0f4d3 [file] [edit]
/* -*- 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 (&notify_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 (&notify_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, &notify_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 (&notify_singleton);
if (!Shell_NotifyIcon (NIM_ADD, &notify_singleton))
{
hwnd_state = HWND_STATE_FAILED;
}
else
{
hwnd_state = HWND_STATE_READY;
Shell_NotifyIcon (NIM_SETVERSION, &notify_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 (&notify_singleton);
if (!Shell_NotifyIcon (NIM_ADD, &notify_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, &notify_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;
}