From 9d43941569fc3840fa9306d149461a8439a54d68 Mon Sep 17 00:00:00 2001 From: Eli Zaretskii Date: Wed, 11 Nov 2015 18:29:36 +0200 Subject: [PATCH] Implement tray notifications for MS-Windows * src/w32fns.c (MY_NOTIFYICONDATAW): New typedef. (NOTIFYICONDATAW_V1_SIZE, NOTIFYICONDATAW_V2_SIZE) (NOTIFYICONDATAW_V3_SIZE, NIF_INFO, NIIF_NONE, NIIF_INFO) (NIIF_WARNING, NIIF_ERROR, EMACS_TRAY_NOTIFICATION_ID) (EMACS_NOTIFICATION_MSG): New macros. (NI_Severity): New enumeration. (get_dll_version, utf8_mbslen_lim, add_tray_notification) (delete_tray_notification, Fw32_notification_notify) (Fw32_notification_close): New functions. (syms_of_w32fns): Defsubr functions exposed to Lisp. DEFSYM keywords used by w32-notification-notify. * doc/lispref/os.texi (Desktop Notifications): Describe the native w32 tray notifications. --- doc/lispref/os.texi | 88 +++++++- src/w32fns.c | 478 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 562 insertions(+), 4 deletions(-) diff --git a/doc/lispref/os.texi b/doc/lispref/os.texi index 7050df86a18..1345ed25441 100644 --- a/doc/lispref/os.texi +++ b/doc/lispref/os.texi @@ -2323,10 +2323,11 @@ Emacs is restarted by the session manager. @cindex notifications, on desktop Emacs is able to send @dfn{notifications} on systems that support the -freedesktop.org Desktop Notifications Specification. In order to use -this functionality, Emacs must have been compiled with D-Bus support, -and the @code{notifications} library must be loaded. @xref{Top, , -D-Bus,dbus,D-Bus integration in Emacs}. +freedesktop.org Desktop Notifications Specification and on MS-Windows. +In order to use this functionality on Posix hosts, Emacs must have +been compiled with D-Bus support, and the @code{notifications} library +must be loaded. @xref{Top, , D-Bus,dbus,D-Bus integration in Emacs}. +The following function is supported when D-Bus support is available: @defun notifications-notify &rest params This function sends a notification to the desktop via D-Bus, @@ -2559,6 +2560,85 @@ If @var{spec_version} is @code{nil}, the server supports a specification prior to @samp{"1.0"}. @end defun +@cindex tray notifications, MS-Windows +When Emacs runs on MS-Windows as a GUI session, it supports a small +subset of the D-Bus notifications functionality via a native +primitive: + +@defun w32-notification-notify &rest params +This function displays an MS-Windows tray notification as specified by +@var{params}. MS-Windows tray notifications are displayed in a +balloon from an icon in the notification area of the taskbar. + +Value is the integer unique ID of the notification that can be used to +remove the notification using @code{w32-notification-close}, described +below. If the function fails, the return value is @code{nil}. + +The arguments @var{params} are specified as keyword/value pairs. All the +parameters are optional, but if no parameters are specified, the +function will do nothing and return @code{nil}. + +The following parameters are supported: + +@table @code +@item :icon @var{icon} +Display @var{icon} in the system tray. If @var{icon} is a string, it +should specify a file name from which to load the icon; the specified +file should be a @file{.ico} Windows icon file. If @var{icon} is not +a string, or if this parameter is not specified, the standard Emacs +icon will be used. + +@item :tip @var{tip} +Use @var{tip} as the tooltip for the notification. If @var{tip} is a +string, this is the text of a tooltip that will be shown when the +mouse pointer hovers over the tray icon added by the notification. If +@var{tip} is not a string, or if this parameter is not specified, the +default tooltip text is @samp{Emacs notification}. The tooltip text can +be up to 127 characters long (63 on Windows versions before W2K). +Longer strings will be truncated. + +@item :level @var{level} +Notification severity level, one of @code{info}, @code{warning}, or +@code{error}. If given, the value determines the icon displayed to the +left of the notification title, but only if the @code{:title} parameter +(see below) is also specified and is a string. + +@item :timeout @var{timeout} +@var{timeout} is the time in seconds after which the notification +disappears. The value can be integer or floating-point. This is +ignored on Vista and later systems, where the duration is fixed at 9 +sec and can only be customized via system-wide Accessibility settings. + +@item :title @var{title} +The title of the notification. If @var{title} is a string, it is +displayed in a larger font immediately above the body text. The title +text can be up to 63 characters long; longer text will be truncated. + +@item :body @var{body} +The body of the notification. If @var{body} is a string, it specifies +the text of the notification message. Use embedded newlines to +control how the text is broken into lines. The body text can be up to +255 characters long, and will be truncated if it's longer. Unlike +with D-Bus, the body text should be plain text, with no markup. +@end table + +Note that versions of Windows before W2K support only @code{:icon} and +@code{:tip}. The other parameters can be passed, but they will be +ignored on those old systems. + +There can be at most one active notification at any given time. An +active notification must be removed by calling +@code{w32-notification-close} before a new one can be shown. +@end defun + +To remove the notification and its icon from the taskbar, use the +following function: + +@defun w32-notification-close id +This function removes the tray notification given by its unique +@var{id}. +@end defun + @node File Notifications @section Notifications on File Changes @cindex file notifications diff --git a/src/w32fns.c b/src/w32fns.c index d92352a9802..eed849f1034 100644 --- a/src/w32fns.c +++ b/src/w32fns.c @@ -55,6 +55,7 @@ along with GNU Emacs. If not, see . */ #include #include #include +#include #include #include #include @@ -8755,6 +8756,473 @@ Internal use only. */) return menubar_in_use ? Qt : Qnil; } +/*********************************************************************** + Tray notifications + ***********************************************************************/ +/* A private struct declaration to avoid compile-time limits. */ +typedef struct MY_NOTIFYICONDATAW { + DWORD cbSize; + HWND hWnd; + UINT uID; + UINT uFlags; + UINT uCallbackMessage; + HICON hIcon; + WCHAR szTip[128]; + DWORD dwState; + DWORD dwStateMask; + WCHAR szInfo[256]; + _ANONYMOUS_UNION union { + UINT uTimeout; + UINT uVersion; + } DUMMYUNIONNAME; + WCHAR szInfoTitle[64]; + DWORD dwInfoFlags; + GUID guidItem; + HICON hBalloonIcon; +} MY_NOTIFYICONDATAW; + +#ifndef NOTIFYICONDATAW_V1_SIZE +# define NOTIFYICONDATAW_V1_SIZE offsetof (MY_NOTIFYICONDATAW, szTip[64]) +#endif +#ifndef NOTIFYICONDATAW_V2_SIZE +# define NOTIFYICONDATAW_V2_SIZE offsetof (MY_NOTIFYICONDATAW, guidItem) +#endif +#ifndef NOTIFYICONDATAW_V3_SIZE +# define NOTIFYICONDATAW_V3_SIZE offsetof (MY_NOTIFYICONDATAW, hBalloonIcon) +#endif +#ifndef NIF_INFO +# define NIF_INFO 0x00000010 +#endif +#ifndef NIIF_NONE +# define NIIF_NONE 0x00000000 +#endif +#ifndef NIIF_INFO +# define NIIF_INFO 0x00000001 +#endif +#ifndef NIIF_WARNING +# define NIIF_WARNING 0x00000002 +#endif +#ifndef NIIF_ERROR +# define NIIF_ERROR 0x00000003 +#endif + + +#define EMACS_TRAY_NOTIFICATION_ID 42 /* arbitrary */ +#define EMACS_NOTIFICATION_MSG (WM_APP + 1) + +enum NI_Severity { + Ni_None, + Ni_Info, + Ni_Warn, + Ni_Err +}; + +/* Report the version of a DLL given by its name. The return value is + constructed using MAKEDLLVERULL. */ +static ULONGLONG +get_dll_version (const char *dll_name) +{ + ULONGLONG version = 0; + HINSTANCE hdll = LoadLibrary (dll_name); + + if (hdll) + { + DLLGETVERSIONPROC pDllGetVersion + = (DLLGETVERSIONPROC) GetProcAddress (hdll, "DllGetVersion"); + + if (pDllGetVersion) + { + DLLVERSIONINFO dvi; + HRESULT result; + + memset (&dvi, 0, sizeof(dvi)); + dvi.cbSize = sizeof(dvi); + result = pDllGetVersion (&dvi); + if (SUCCEEDED (result)) + version = MAKEDLLVERULL (dvi.dwMajorVersion, dvi.dwMinorVersion, + 0, 0); + } + FreeLibrary (hdll); + } + + return version; +} + +/* Return the number of bytes in UTF-8 encoded string STR that + corresponds to at most LIM characters. If STR ends before LIM + characters, return the number of bytes in STR including the + terminating null byte. */ +static int +utf8_mbslen_lim (const char *str, int lim) +{ + const char *p = str; + int mblen = 0, nchars = 0; + + while (*p && nchars < lim) + { + int nbytes = CHAR_BYTES (*p); + + mblen += nbytes; + nchars++; + p += nbytes; + } + + if (!*p && nchars < lim) + mblen++; + + return mblen; +} + +/* Low-level subroutine to show tray notifications. All strings are + supposed to be unibyte UTF-8 encoded by the caller. */ +static EMACS_INT +add_tray_notification (struct frame *f, const char *icon, const char *tip, + enum NI_Severity severity, unsigned timeout, + const char *title, const char *msg) +{ + EMACS_INT retval = EMACS_TRAY_NOTIFICATION_ID; + + if (FRAME_W32_P (f)) + { + MY_NOTIFYICONDATAW nidw; + ULONGLONG shell_dll_version = get_dll_version ("Shell32.dll"); + wchar_t tipw[128], msgw[256], titlew[64]; + int tiplen; + + memset (&nidw, 0, sizeof(nidw)); + + /* MSDN says the full struct is supported since Vista, whose + Shell32.dll version is said to be 6.0.6. But DllGetVersion + cannot report the 3rd field value, it reports "build number" + instead, which is something else. So we use the Windows 7's + version 6.1 as cutoff, and Vista loses. (Actually, the loss + is not a real one, since we don't expose the hBalloonIcon + member of the struct to Lisp.) */ + if (shell_dll_version >= MAKEDLLVERULL (6, 1, 0, 0)) /* >= Windows 7 */ + nidw.cbSize = sizeof (nidw); + else if (shell_dll_version >= MAKEDLLVERULL (6, 0, 0, 0)) /* XP */ + nidw.cbSize = NOTIFYICONDATAW_V3_SIZE; + else if (shell_dll_version >= MAKEDLLVERULL (5, 0, 0, 0)) /* W2K */ + nidw.cbSize = NOTIFYICONDATAW_V2_SIZE; + else + nidw.cbSize = NOTIFYICONDATAW_V1_SIZE; /* < W2K */ + nidw.hWnd = FRAME_W32_WINDOW (f); + nidw.uID = EMACS_TRAY_NOTIFICATION_ID; + nidw.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP | NIF_INFO; + nidw.uCallbackMessage = EMACS_NOTIFICATION_MSG; + if (!*icon) + nidw.hIcon = LoadIcon (hinst, EMACS_CLASS); + else + { + if (w32_unicode_filenames) + { + wchar_t icon_w[MAX_PATH]; + + if (filename_to_utf16 (icon, icon_w) != 0) + { + errno = ENOENT; + return -1; + } + nidw.hIcon = LoadImageW (NULL, icon_w, IMAGE_ICON, 0, 0, + LR_DEFAULTSIZE | LR_LOADFROMFILE); + } + else + { + char icon_a[MAX_PATH]; + + if (filename_to_ansi (icon, icon_a) != 0) + { + errno = ENOENT; + return -1; + } + nidw.hIcon = LoadImageA (NULL, icon_a, IMAGE_ICON, 0, 0, + LR_DEFAULTSIZE | LR_LOADFROMFILE); + } + } + if (!nidw.hIcon) + { + switch (GetLastError ()) + { + case ERROR_FILE_NOT_FOUND: + errno = ENOENT; + break; + default: + errno = ENOMEM; + break; + } + return -1; + } + + /* Windows 9X and NT4 support only 64 characters in the Tip, + later versions support up to 128. */ + if (nidw.cbSize == NOTIFYICONDATAW_V1_SIZE) + { + tiplen = pMultiByteToWideChar (CP_UTF8, MB_ERR_INVALID_CHARS, + tip, utf8_mbslen_lim (tip, 63), + tipw, 64); + if (tiplen >= 63) + tipw[63] = 0; + } + else + { + tiplen = pMultiByteToWideChar (CP_UTF8, MB_ERR_INVALID_CHARS, + tip, utf8_mbslen_lim (tip, 127), + tipw, 128); + if (tiplen >= 127) + tipw[127] = 0; + } + if (tiplen == 0) + { + errno = EINVAL; + retval = -1; + goto done; + } + wcscpy (nidw.szTip, tipw); + + /* The rest of the structure is only supported since Windows 2000. */ + if (nidw.cbSize > NOTIFYICONDATAW_V1_SIZE) + { + int slen; + + slen = pMultiByteToWideChar (CP_UTF8, MB_ERR_INVALID_CHARS, + msg, utf8_mbslen_lim (msg, 255), + msgw, 256); + if (slen >= 255) + msgw[255] = 0; + else if (slen == 0) + { + errno = EINVAL; + retval = -1; + goto done; + } + wcscpy (nidw.szInfo, msgw); + nidw.uTimeout = timeout; + slen = pMultiByteToWideChar (CP_UTF8, MB_ERR_INVALID_CHARS, + title, utf8_mbslen_lim (title, 63), + titlew, 64); + if (slen >= 63) + titlew[63] = 0; + else if (slen == 0) + { + errno = EINVAL; + retval = -1; + goto done; + } + wcscpy (nidw.szInfoTitle, titlew); + + switch (severity) + { + case Ni_None: + nidw.dwInfoFlags = NIIF_NONE; + break; + case Ni_Info: + default: + nidw.dwInfoFlags = NIIF_INFO; + break; + case Ni_Warn: + nidw.dwInfoFlags = NIIF_WARNING; + break; + case Ni_Err: + nidw.dwInfoFlags = NIIF_ERROR; + break; + } + } + + if (!Shell_NotifyIconW (NIM_ADD, (PNOTIFYICONDATAW)&nidw)) + { + /* GetLastError returns meaningless results when + Shell_NotifyIcon fails. */ + DebPrint (("Shell_NotifyIcon ADD failed (err=%d)\n", + GetLastError ())); + errno = EINVAL; + retval = -1; + } + done: + if (*icon && !DestroyIcon (nidw.hIcon)) + DebPrint (("DestroyIcon failed (err=%d)\n", GetLastError ())); + } + return retval; +} + +/* Low-level subroutine to remove a tray notification. Note: we only + pass the minimum data about the notification: its ID and the handle + of the window to which it sends messages. MSDN doesn't say this is + enough, but it works in practice. This allows us to avoid keeping + the notification data around after we show the notification. */ +static void +delete_tray_notification (struct frame *f, int id) +{ + if (FRAME_W32_P (f)) + { + MY_NOTIFYICONDATAW nidw; + + memset (&nidw, 0, sizeof(nidw)); + nidw.hWnd = FRAME_W32_WINDOW (f); + nidw.uID = id; + + if (!Shell_NotifyIconW (NIM_DELETE, (PNOTIFYICONDATAW)&nidw)) + { + /* GetLastError returns meaningless results when + Shell_NotifyIcon fails. */ + DebPrint (("Shell_NotifyIcon DELETE failed\n")); + errno = EINVAL; + return; + } + } + return; +} + +DEFUN ("w32-notification-notify", + Fw32_notification_notify, Sw32_notification_notify, + 0, MANY, 0, + doc: /* Display an MS-Windows tray notification as specified by PARAMS. + +Value is the integer unique ID of the notification that can be used +to remove the notification using `w32-notification-close', which see. +If the function fails, the return value is nil. + +Tray notifications, a.k.a. \"taskbar messages\", are messages that +inform the user about events unrelated to the current user activity, +such as a significant system event, by briefly displaying informative +text in a balloon from an icon in the notification area of the taskbar. + +Parameters in PARAMS are specified as keyword/value pairs. All the +parameters are optional, but if no parameters are specified, the +function will do nothing and return nil. + +The following parameters are supported: + +:icon ICON -- Display ICON in the system tray. If ICON is a string, + it should specify a file name from which to load the + icon; the specified file should be a .ico Windows icon + file. If ICON is not a string, or if this parameter + is not specified, the standard Emacs icon will be used. + +:tip TIP -- Use TIP as the tooltip for the notification. If TIP + is a string, this is the text of a tooltip that will + be shown when the mouse pointer hovers over the tray + icon added by the notification. If TIP is not a + string, or if this parameter is not specified, the + default tooltip text is \"Emacs notification\". The + tooltip text can be up to 127 characters long (63 + on Windows versions before W2K). Longer strings + will be truncated. + +:level LEVEL -- Notification severity level, one of `info', + `warning', or `error'. If given, the value + determines the icon displayed to the left of the + notification title, but only if the `:title' + parameter (see below) is also specified and is a + string. + +:timeout TIMEOUT -- TIMEOUT is the time in seconds after which the + notification disappears. The value can be integer + or floating-point. This is ignored on Vista and + later systems, where the duration is fixed at 9 sec + and can only be customized via system-wide + Accessibility settings. + +:title TITLE -- The title of the notification. If TITLE is a string, + it is displayed in a larger font immediately above + the body text. The title text can be up to 63 + characters long; longer text will be truncated. + +:body BODY -- The body of the notification. If BODY is a string, + it specifies the text of the notification message. + Use embedded newlines to control how the text is + broken into lines. The body text can be up to 255 + characters long, and will be truncated if it's longer. + +Note that versions of Windows before W2K support only `:icon' and `:tip'. +You can pass the other parameters, but they will be ignored on those +old systems. + +There can be at most one active notification at any given time. An +active notification must be removed by calling `w32-notification-close' +before a new one can be shown. + +usage: (w32-notification-notify &rest PARAMS) */) + (ptrdiff_t nargs, Lisp_Object *args) +{ + struct frame *f = SELECTED_FRAME (); + Lisp_Object arg_plist, lres; + EMACS_INT retval; + char *icon, *tip, *title, *msg; + enum NI_Severity severity; + unsigned timeout; + + if (nargs == 0) + return Qnil; + + arg_plist = Flist (nargs, args); + + /* Icon. */ + lres = Fplist_get (arg_plist, QCicon); + if (STRINGP (lres)) + icon = SSDATA (ENCODE_FILE (Fexpand_file_name (lres, Qnil))); + else + icon = ""; + + /* Tip. */ + lres = Fplist_get (arg_plist, QCtip); + if (STRINGP (lres)) + tip = SSDATA (code_convert_string_norecord (lres, Qutf_8, 1)); + else + tip = "Emacs notification"; + + /* Severity. */ + lres = Fplist_get (arg_plist, QClevel); + if (NILP (lres)) + severity = Ni_None; + else if (EQ (lres, Qinfo)) + severity = Ni_Info; + else if (EQ (lres, Qwarning)) + severity = Ni_Warn; + else if (EQ (lres, Qerror)) + severity = Ni_Err; + else + severity = Ni_Info; + + /* Timeout. */ + lres = Fplist_get (arg_plist, QCtimeout); + if (NUMBERP (lres)) + timeout = 1000 * (INTEGERP (lres) ? XINT (lres) : XFLOAT_DATA (lres)); + else + timeout = 0; + + /* Title. */ + lres = Fplist_get (arg_plist, QCtitle); + if (STRINGP (lres)) + title = SSDATA (code_convert_string_norecord (lres, Qutf_8, 1)); + else + title = ""; + + /* Notification body text. */ + lres = Fplist_get (arg_plist, QCbody); + if (STRINGP (lres)) + msg = SSDATA (code_convert_string_norecord (lres, Qutf_8, 1)); + else + msg = ""; + + /* Do it! */ + retval = add_tray_notification (f, icon, tip, severity, timeout, title, msg); + return (retval < 0 ? Qnil : make_number (retval)); +} + +DEFUN ("w32-notification-close", + Fw32_notification_close, Sw32_notification_close, + 1, 1, 0, + doc: /* Remove the MS-Windows tray notification specified by its ID. */) + (Lisp_Object id) +{ + struct frame *f = SELECTED_FRAME (); + + if (INTEGERP (id)) + delete_tray_notification (f, XINT (id)); + + return Qnil; +} + /*********************************************************************** Initialization @@ -8828,6 +9296,14 @@ syms_of_w32fns (void) DEFSYM (Qframes, "frames"); DEFSYM (Qtip_frame, "tip-frame"); DEFSYM (Qunicode_sip, "unicode-sip"); + DEFSYM (QCicon, ":icon"); + DEFSYM (QCtip, ":tip"); + DEFSYM (QClevel, ":level"); + DEFSYM (Qinfo, "info"); + DEFSYM (Qwarning, "warning"); + DEFSYM (QCtimeout, ":timeout"); + DEFSYM (QCtitle, ":title"); + DEFSYM (QCbody, ":body"); /* Symbols used elsewhere, but only in MS-Windows-specific code. */ DEFSYM (Qgnutls_dll, "gnutls"); @@ -9161,6 +9637,8 @@ This variable has effect only on Windows Vista and later. */); defsubr (&Sw32_window_exists_p); defsubr (&Sw32_battery_status); defsubr (&Sw32__menu_bar_in_use); + defsubr (&Sw32_notification_notify); + defsubr (&Sw32_notification_close); #ifdef WINDOWSNT defsubr (&Sfile_system_info); -- 2.39.2