diff --git a/CMakeLists.txt b/CMakeLists.txt index abcf5ab..8fecb9f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,7 @@ else() list(APPEND SRCS ${CMAKE_CURRENT_SOURCE_DIR}/tray_darwin.m) else() find_package(APPINDICATOR REQUIRED) + find_package(LIBNOTIFY REQUIRED) list(APPEND SRCS ${CMAKE_CURRENT_SOURCE_DIR}/tray_linux.c) endif() endif() @@ -48,7 +49,14 @@ else() target_compile_options(tray PRIVATE ${APPINDICATOR_CFLAGS}) target_link_directories(tray PRIVATE ${APPINDICATOR_LIBRARY_DIRS}) target_compile_definitions(tray PRIVATE TRAY_APPINDICATOR=1) - target_link_libraries(tray PRIVATE ${APPINDICATOR_LIBRARIES}) + if(APPINDICATOR_AYATANA) + target_compile_definitions(tray PRIVATE TRAY_AYATANA_APPINDICATOR=1) + endif() + if(APPINDICATOR_LEGACY) + target_compile_definitions(tray PRIVATE TRAY_LEGACY_APPINDICATOR=1) + endif() + target_compile_definitions(tray PRIVATE TRAY_LIBNOTIFY=1) + target_link_libraries(tray PRIVATE ${APPINDICATOR_LIBRARIES} ${LIBNOTIFY_LIBRARIES}) endif() endif() endif() diff --git a/cmake/FindAPPINDICATOR.cmake b/cmake/FindAPPINDICATOR.cmake index 5d9f718..6bfeb66 100644 --- a/cmake/FindAPPINDICATOR.cmake +++ b/cmake/FindAPPINDICATOR.cmake @@ -23,7 +23,12 @@ include(FindPackageHandleStandardArgs) PKG_CHECK_MODULES(APPINDICATOR ayatana-appindicator3-0.1) IF( APPINDICATOR_FOUND ) - SET(HAVE_AYATANAAPPINDICATOR 1) + SET(APPINDICATOR_AYATANA 1) +ELSE() + PKG_CHECK_MODULES(APPINDICATOR appindicator3-0.1) + IF( APPINDICATOR_FOUND ) + SET(APPINDICATOR_LEGACY 1) + ENDIF() ENDIF() mark_as_advanced(APPINDICATOR_INCLUDE_DIR APPINDICATOR_LIBRARY) diff --git a/cmake/FindLIBNOTIFY.cmake b/cmake/FindLIBNOTIFY.cmake new file mode 100644 index 0000000..e76b199 --- /dev/null +++ b/cmake/FindLIBNOTIFY.cmake @@ -0,0 +1,55 @@ +# - Try to find LibNotify +# This module defines the following variables: +# +# LIBNOTIFY_FOUND - LibNotify was found +# LIBNOTIFY_INCLUDE_DIRS - the LibNotify include directories +# LIBNOTIFY_LIBRARIES - link these to use LibNotify +# +# Copyright (C) 2012 Raphael Kubo da Costa +# Copyright (C) 2014 Collabora Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND ITS CONTRIBUTORS ``AS +# IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR ITS +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +find_package(PkgConfig) +pkg_check_modules(LIBNOTIFY QUIET libnotify) + +find_path(LIBNOTIFY_INCLUDE_DIRS + NAMES notify.h + HINTS ${LIBNOTIFY_INCLUDEDIR} + ${LIBNOTIFY_INCLUDE_DIRS} + PATH_SUFFIXES libnotify +) + +find_library(LIBNOTIFY_LIBRARIES + NAMES notify + HINTS ${LIBNOTIFY_LIBDIR} + ${LIBNOTIFY_LIBRARY_DIRS} +) + +include(FindPackageHandleStandardArgs) +FIND_PACKAGE_HANDLE_STANDARD_ARGS(LibNotify REQUIRED_VARS LIBNOTIFY_INCLUDE_DIRS LIBNOTIFY_LIBRARIES + VERSION_VAR LIBNOTIFY_VERSION) + +mark_as_advanced( + LIBNOTIFY_INCLUDE_DIRS + LIBNOTIFY_LIBRARIES +) diff --git a/tray.h b/tray.h index 26d1426..68b5324 100644 --- a/tray.h +++ b/tray.h @@ -10,8 +10,14 @@ struct tray_menu; struct tray { const char *icon; - char *tooltip; + const char *tooltip; + const char *notification_icon; + const char *notification_text; + const char *notification_title; + void (*notification_cb)(); struct tray_menu *menu; + const int iconPathCount; + const char *allIconPaths[]; }; struct tray_menu { diff --git a/tray_linux.c b/tray_linux.c index 1ed6716..fc643c9 100644 --- a/tray_linux.c +++ b/tray_linux.c @@ -1,13 +1,26 @@ #include "tray.h" #include #include +#include +#ifdef TRAY_AYATANA_APPINDICATOR #include +#elif TRAY_LEGACY_APPINDICATOR +#include +#endif +#ifndef IS_APP_INDICATOR +#define IS_APP_INDICATOR APP_IS_INDICATOR +#endif +#include #define TRAY_APPINDICATOR_ID "tray-id" -static AppIndicator *indicator = NULL; -static int loop_result = 0; +static bool async_update_pending = false; +static pthread_cond_t async_update_cv = PTHREAD_COND_INITIALIZER; +static pthread_mutex_t async_update_mutex = PTHREAD_MUTEX_INITIALIZER; +static AppIndicator *indicator = NULL; +static int loop_result = 0; +static NotifyNotification *currentNotification = NULL; static void _tray_menu_cb(GtkMenuItem *item, gpointer data) { (void)item; struct tray_menu *m = (struct tray_menu *)data; @@ -16,23 +29,26 @@ static void _tray_menu_cb(GtkMenuItem *item, gpointer data) { static GtkMenuShell *_tray_menu(struct tray_menu *m) { GtkMenuShell *menu = (GtkMenuShell *)gtk_menu_new(); - for (; m != NULL && m->text != NULL; m++) { + for(; m != NULL && m->text != NULL; m++) { GtkWidget *item; - if (strcmp(m->text, "-") == 0) { + if(strcmp(m->text, "-") == 0) { item = gtk_separator_menu_item_new(); - } else { - if (m->submenu != NULL) { + } + else { + if(m->submenu != NULL) { item = gtk_menu_item_new_with_label(m->text); gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), - GTK_WIDGET(_tray_menu(m->submenu))); - } else if (m->checkbox) { + GTK_WIDGET(_tray_menu(m->submenu))); + } + else if(m->checkbox) { item = gtk_check_menu_item_new_with_label(m->text); gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), !!m->checked); - } else { + } + else { item = gtk_menu_item_new_with_label(m->text); } gtk_widget_set_sensitive(item, !m->disabled); - if (m->cb != NULL) { + if(m->cb != NULL) { g_signal_connect(item, "activate", G_CALLBACK(_tray_menu_cb), m); } } @@ -43,11 +59,13 @@ static GtkMenuShell *_tray_menu(struct tray_menu *m) { } int tray_init(struct tray *tray) { - if (gtk_init_check(0, NULL) == FALSE) { + if(gtk_init_check(0, NULL) == FALSE) { return -1; } + notify_init("tray-icon"); indicator = app_indicator_new(TRAY_APPINDICATOR_ID, tray->icon, - APP_INDICATOR_CATEGORY_APPLICATION_STATUS); + APP_INDICATOR_CATEGORY_APPLICATION_STATUS); + if(indicator == NULL || !IS_APP_INDICATOR(indicator))return -1; app_indicator_set_status(indicator, APP_INDICATOR_STATUS_ACTIVE); tray_update(tray); return 0; @@ -58,12 +76,87 @@ int tray_loop(int blocking) { return loop_result; } +static gboolean tray_update_internal(gpointer user_data) { + struct tray *tray = user_data; + + if(indicator != NULL && IS_APP_INDICATOR(indicator)){ + app_indicator_set_icon(indicator, tray->icon); + // GTK is all about reference counting, so previous menu should be destroyed + // here + app_indicator_set_menu(indicator, GTK_MENU(_tray_menu(tray->menu))); + } + if(tray->notification_text != 0 && strlen(tray->notification_text) > 0 && notify_is_initted()) { + if(currentNotification != NULL && NOTIFY_IS_NOTIFICATION(currentNotification)){ + notify_notification_close(currentNotification,NULL); + g_object_unref(G_OBJECT(currentNotification)); + } + const char *notification_icon = tray->notification_icon != NULL ? tray->notification_icon : tray->icon; + currentNotification = notify_notification_new(tray->notification_title, tray->notification_text, notification_icon); + if(currentNotification != NULL && NOTIFY_IS_NOTIFICATION(currentNotification)){ + if(tray->notification_cb != NULL){ + notify_notification_add_action(currentNotification,"default","Default",tray->notification_cb,NULL,NULL); + } + notify_notification_show(currentNotification, NULL); + } + } + + // Unwait any pending tray_update() calls + pthread_mutex_lock(&async_update_mutex); + async_update_pending = false; + pthread_cond_broadcast(&async_update_cv); + pthread_mutex_unlock(&async_update_mutex); + return G_SOURCE_REMOVE; +} + void tray_update(struct tray *tray) { - app_indicator_set_icon(indicator, tray->icon); - // GTK is all about reference counting, so previous menu should be destroyed - // here - app_indicator_set_menu(indicator, GTK_MENU(_tray_menu(tray->menu))); + // Perform the tray update on the tray loop thread, but block + // in this thread to ensure none of the strings stored in the + // tray icon struct go out of scope before the callback runs. + + if (g_main_context_is_owner(g_main_context_default())) { + // Invoke the callback directly if we're on the loop thread + tray_update_internal(tray); + } + else { + // If there's already an update pending, wait for it to complete + // and claim the next pending update slot. + pthread_mutex_lock(&async_update_mutex); + while (async_update_pending) { + pthread_cond_wait(&async_update_cv, &async_update_mutex); + } + async_update_pending = true; + pthread_mutex_unlock(&async_update_mutex); + + // Queue the update callback to the tray thread + g_main_context_invoke(NULL, tray_update_internal, tray); + + // Wait for the callback to run + pthread_mutex_lock(&async_update_mutex); + while (async_update_pending) { + pthread_cond_wait(&async_update_cv, &async_update_mutex); + } + pthread_mutex_unlock(&async_update_mutex); + } +} + +static gboolean tray_exit_internal(gpointer user_data) { + if(currentNotification != NULL && NOTIFY_IS_NOTIFICATION(currentNotification)){ + int v = notify_notification_close(currentNotification,NULL); + if(v == TRUE)g_object_unref(G_OBJECT(currentNotification)); + } + notify_uninit(); + return G_SOURCE_REMOVE; } -void tray_exit(void) { loop_result = -1; } +void tray_exit(void) { + // Wait for any pending callbacks to complete + pthread_mutex_lock(&async_update_mutex); + while (async_update_pending) { + pthread_cond_wait(&async_update_cv, &async_update_mutex); + } + pthread_mutex_unlock(&async_update_mutex); + // Perform cleanup on the main thread + loop_result = -1; + g_main_context_invoke(NULL, tray_exit_internal, NULL); +} diff --git a/tray_windows.c b/tray_windows.c index c2d078d..7176291 100644 --- a/tray_windows.c +++ b/tray_windows.c @@ -6,12 +6,29 @@ #define WC_TRAY_CLASS_NAME "TRAY" #define ID_TRAY_FIRST 1000 +struct icon_info { + const char *path; + HICON icon; + HICON large_icon; + HICON notification_icon; +}; + +enum IconType { + REGULAR = 1, + LARGE, + NOTIFICATION +}; + static WNDCLASSEX wc; static NOTIFYICONDATA nid; static HWND hwnd; static HMENU hmenu = NULL; +static void (*notification_cb)() = 0; static UINT wm_taskbarcreated; +static struct icon_info *icon_infos; +static int icon_info_count; + static LRESULT CALLBACK _tray_wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { switch (msg) { @@ -31,6 +48,8 @@ static LRESULT CALLBACK _tray_wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, p.x, p.y, 0, hwnd, NULL); SendMessage(hwnd, WM_COMMAND, cmd, 0); return 0; + } else if(lparam == NIN_BALLOONUSERCLICK && notification_cb != NULL){ + notification_cb(); } break; case WM_COMMAND: @@ -89,9 +108,74 @@ static HMENU _tray_menu(struct tray_menu *m, UINT *id) { return hmenu; } +struct icon_info _create_icon_info(const char * path) { + struct icon_info info; + info.path = strdup(path); + + // These must be separate invocations otherwise Windows may opt to only return large or small icons. + // MSDN does not explicitly state this anywhere, but it has been observed on some machines. + ExtractIconEx(path, 0, &info.large_icon, NULL, 1); + ExtractIconEx(path, 0, NULL, &info.icon, 1); + + info.notification_icon = LoadImageA(NULL, path, IMAGE_ICON, GetSystemMetrics(SM_CXICON) * 2, GetSystemMetrics(SM_CYICON) * 2, LR_LOADFROMFILE); + return info; +} + +void _init_icon_cache(const char ** paths, int count) { + icon_info_count = count; + icon_infos = malloc(sizeof(struct icon_info) * icon_info_count); + + for (int i = 0; i < count; ++i) { + icon_infos[i] = _create_icon_info(paths[i]); + } +} + +void _destroy_icon_cache() { + for (int i = 0; i < icon_info_count; ++i) { + DestroyIcon(icon_infos[i].icon); + DestroyIcon(icon_infos[i].large_icon); + DestroyIcon(icon_infos[i].notification_icon); + free((void*) icon_infos[i].path); + } + + free(icon_infos); + icon_infos = NULL; + icon_info_count = 0; +} + +HICON _fetch_cached_icon(struct icon_info *icon_record, enum IconType icon_type) { + switch (icon_type) { + case REGULAR: + return icon_record->icon; + case LARGE: + return icon_record->large_icon; + case NOTIFICATION: + return icon_record->notification_icon; + } +} + +HICON _fetch_icon(const char * path, enum IconType icon_type) { + // Find a cached icon by path + for (int i = 0; i < icon_info_count; ++i) { + if (strcmp(icon_infos[i].path, path) == 0) { + return _fetch_cached_icon(&icon_infos[i], icon_type); + } + } + + // Expand cache, fetch, and retry + icon_info_count += 1; + icon_infos = realloc(icon_infos, sizeof(struct icon_info) * icon_info_count); + int index = icon_info_count - 1; + icon_infos[icon_info_count - 1] = _create_icon_info(path); + + return _fetch_cached_icon(&icon_infos[icon_info_count - 1], icon_type); +} + int tray_init(struct tray *tray) { wm_taskbarcreated = RegisterWindowMessage("TaskbarCreated"); + _init_icon_cache(tray->allIconPaths, tray->iconPathCount); + memset(&wc, 0, sizeof(wc)); wc.cbSize = sizeof(WNDCLASSEX); wc.lpfnWndProc = _tray_wnd_proc; @@ -135,20 +219,46 @@ int tray_loop(int blocking) { } void tray_update(struct tray *tray) { - HMENU prevmenu = hmenu; UINT id = ID_TRAY_FIRST; + HMENU prevmenu = hmenu; hmenu = _tray_menu(tray->menu, &id); SendMessage(hwnd, WM_INITMENUPOPUP, (WPARAM)hmenu, 0); - HICON icon; - ExtractIconEx(tray->icon, 0, NULL, &icon, 1); - if (nid.hIcon) { - DestroyIcon(nid.hIcon); + + HICON icon = _fetch_icon(tray->icon, REGULAR); + HICON largeIcon = tray->notification_icon != 0 + ? _fetch_icon(tray->notification_icon, NOTIFICATION) + : _fetch_icon(tray->icon, LARGE); + + if (icon != NULL) { + nid.hIcon = icon; + } + + if(largeIcon != 0){ + nid.hBalloonIcon = largeIcon; + nid.dwInfoFlags = NIIF_USER | NIIF_LARGE_ICON; } - nid.hIcon = icon; if(tray->tooltip != 0 && strlen(tray->tooltip) > 0) { strncpy(nid.szTip, tray->tooltip, sizeof(nid.szTip)); nid.uFlags |= NIF_TIP; } + QUERY_USER_NOTIFICATION_STATE notification_state; + HRESULT ns = SHQueryUserNotificationState(¬ification_state); + int can_show_notifications = ns == S_OK && notification_state == QUNS_ACCEPTS_NOTIFICATIONS; + if(can_show_notifications == 1 && tray->notification_title != 0 && strlen(tray->notification_title) > 0){ + strncpy(nid.szInfoTitle, tray->notification_title, sizeof(nid.szInfoTitle)); + nid.uFlags |= NIF_INFO; + } else if((nid.uFlags & NIF_INFO) == NIF_INFO) { + strncpy(nid.szInfoTitle, "", sizeof(nid.szInfoTitle)); + } + if(can_show_notifications == 1 && tray->notification_text != 0 && strlen(tray->notification_text) > 0){ + strncpy(nid.szInfo, tray->notification_text, sizeof(nid.szInfo)); + } else if((nid.uFlags & NIF_INFO) == NIF_INFO) { + strncpy(nid.szInfo, "", sizeof(nid.szInfo)); + } + if(can_show_notifications == 1 && tray->notification_cb != NULL){ + notification_cb = tray->notification_cb; + } + Shell_NotifyIcon(NIM_MODIFY, &nid); if (prevmenu != NULL) { @@ -158,9 +268,7 @@ void tray_update(struct tray *tray) { void tray_exit(void) { Shell_NotifyIcon(NIM_DELETE, &nid); - if (nid.hIcon != 0) { - DestroyIcon(nid.hIcon); - } + _destroy_icon_cache(); if (hmenu != 0) { DestroyMenu(hmenu); }