From 48b2241a2f7fd4b5e650b2b0f2625e185e75c0a1 Mon Sep 17 00:00:00 2001 From: Florian Brinker Date: Sat, 14 Nov 2020 17:40:43 +0100 Subject: [PATCH 1/2] Add checkbox support for Linux --- README.md | 2 +- example/main.go | 4 ++-- systray.go | 42 ++++++++++++++++++++++++++++++++++-------- systray.h | 2 +- systray_darwin.m | 2 +- systray_linux.c | 18 +++++++++++++++--- systray_nonwindows.go | 5 +++++ 7 files changed, 59 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e0ed70d..77677d6 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ On Linux Mint, `libxapp-dev` is also required . To build `webview_example`, you also need to install `libwebkit2gtk-4.0-dev` and remove `webview_example/rsrc.syso` which is required on Windows. -* Submenu and checked menu items are not yet implemented +* Submenu items are not yet implemented ### Windows diff --git a/example/main.go b/example/main.go index 8a02fed..431935c 100644 --- a/example/main.go +++ b/example/main.go @@ -37,7 +37,7 @@ func onReady() { systray.SetTitle("Awesome App") systray.SetTooltip("Pretty awesome棒棒嗒") mChange := systray.AddMenuItem("Change Me", "Change Me") - mChecked := systray.AddMenuItem("Unchecked", "Check Me") + mChecked := systray.AddMenuItemCheckbox("Unchecked", "Check Me", true) mEnabled := systray.AddMenuItem("Enabled", "Enabled") // Sets the icon of a menu item. Only available on Mac. mEnabled.SetTemplateIcon(icon.Data, icon.Data) @@ -46,7 +46,7 @@ func onReady() { subMenuTop := systray.AddMenuItem("SubMenu", "SubMenu Test (top)") subMenuMiddle := subMenuTop.AddSubMenuItem("SubMenu - Level 2", "SubMenu Test (middle)") - subMenuBottom := subMenuMiddle.AddSubMenuItem("SubMenu - Level 3", "SubMenu Test (bottom)") + subMenuBottom := subMenuMiddle.AddSubMenuItemCheckbox("SubMenu - Level 3", "SubMenu Test (bottom)", false) subMenuBottom2 := subMenuMiddle.AddSubMenuItem("Panic!", "SubMenu Test (bottom)") mUrl := systray.AddMenuItem("Open UI", "my home") diff --git a/systray.go b/systray.go index a4a192f..8fe9d16 100644 --- a/systray.go +++ b/systray.go @@ -44,6 +44,8 @@ type MenuItem struct { disabled bool // checked menu item has a tick before the title checked bool + // has the menu item a checkbox (Linux) + isCheckable bool // parent item, for sub menus parent *MenuItem } @@ -58,13 +60,14 @@ func (item *MenuItem) String() string { // newMenuItem returns a populated MenuItem object func newMenuItem(title string, tooltip string, parent *MenuItem) *MenuItem { return &MenuItem{ - ClickedCh: make(chan struct{}), - id: atomic.AddUint32(¤tID, 1), - title: title, - tooltip: tooltip, - disabled: false, - checked: false, - parent: parent, + ClickedCh: make(chan struct{}), + id: atomic.AddUint32(¤tID, 1), + title: title, + tooltip: tooltip, + disabled: false, + checked: false, + isCheckable: false, + parent: parent, } } @@ -109,14 +112,25 @@ func Quit() { } // AddMenuItem adds a menu item with the designated title and tooltip. -// // It can be safely invoked from different goroutines. +// Created menu items are checkable on Windows and OSX by default. For Linux you have to use AddMenuItemCheckbox func AddMenuItem(title string, tooltip string) *MenuItem { item := newMenuItem(title, tooltip, nil) item.update() return item } +// AddMenuItemCheckbox adds a menu item with the designated title and tooltip and a checkbox for Linux. +// It can be safely invoked from different goroutines. +// On Windows and OSX this is the same as calling AddMenuItem +func AddMenuItemCheckbox(title string, tooltip string, checked bool) *MenuItem { + item := newMenuItem(title, tooltip, nil) + item.isCheckable = true + item.checked = checked + item.update() + return item +} + // AddSeparator adds a separator bar to the menu func AddSeparator() { addSeparator(atomic.AddUint32(¤tID, 1)) @@ -124,12 +138,24 @@ func AddSeparator() { // AddSubMenuItem adds a nested sub-menu item with the designated title and tooltip. // It can be safely invoked from different goroutines. +// Created menu items are checkable on Windows and OSX by default. For Linux you have to use AddSubMenuItemCheckbox func (item *MenuItem) AddSubMenuItem(title string, tooltip string) *MenuItem { child := newMenuItem(title, tooltip, item) child.update() return child } +// AddSubMenuItemCheckbox adds a nested sub-menu item with the designated title and tooltip and a checkbox for Linux. +// It can be safely invoked from different goroutines. +// On Windows and OSX this is the same as calling AddSubMenuItem +func (item *MenuItem) AddSubMenuItemCheckbox(title string, tooltip string, checked bool) *MenuItem { + child := newMenuItem(title, tooltip, item) + child.isCheckable = true + child.checked = checked + child.update() + return child +} + // SetTitle set the text to display on a menu item func (item *MenuItem) SetTitle(title string) { item.title = title diff --git a/systray.h b/systray.h index e1a1f98..888c829 100644 --- a/systray.h +++ b/systray.h @@ -10,7 +10,7 @@ void setIcon(const char* iconBytes, int length, bool template); void setMenuItemIcon(const char* iconBytes, int length, int menuId, bool template); void setTitle(char* title); void setTooltip(char* tooltip); -void add_or_update_menu_item(int menuId, int parentMenuId, char* title, char* tooltip, short disabled, short checked); +void add_or_update_menu_item(int menuId, int parentMenuId, char* title, char* tooltip, short disabled, short checked, short isCheckable); void add_separator(int menuId); void hide_menu_item(int menuId); void show_menu_item(int menuId); diff --git a/systray_darwin.m b/systray_darwin.m index d434073..884fa43 100644 --- a/systray_darwin.m +++ b/systray_darwin.m @@ -266,7 +266,7 @@ void setTooltip(char* ctooltip) { runInMainThread(@selector(setTooltip:), (id)tooltip); } -void add_or_update_menu_item(int menuId, int parentMenuId, char* title, char* tooltip, short disabled, short checked) { +void add_or_update_menu_item(int menuId, int parentMenuId, char* title, char* tooltip, short disabled, short checked, short isCheckable) { MenuItem* item = [[MenuItem alloc] initWithId: menuId withParentMenuId: parentMenuId withTitle: title withTooltip: tooltip withDisabled: disabled withChecked: checked]; free(title); free(tooltip); diff --git a/systray_linux.c b/systray_linux.c index 7586f1b..debbf68 100644 --- a/systray_linux.c +++ b/systray_linux.c @@ -21,6 +21,7 @@ typedef struct { char* tooltip; short disabled; short checked; + short isCheckable; } MenuItemInfo; void registerSystray(void) { @@ -90,15 +91,25 @@ gboolean do_add_or_update_menu_item(gpointer data) { GList* it; for(it = global_menu_items; it != NULL; it = it->next) { MenuItemNode* item = (MenuItemNode*)(it->data); - if(item->menu_id == mii->menu_id){ + if(item->menu_id == mii->menu_id) { gtk_menu_item_set_label(GTK_MENU_ITEM(item->menu_item), mii->title); + + if (mii->isCheckable) { + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item->menu_item), mii->checked == 1); + } break; } } // menu id doesn't exist, add new item if(it == NULL) { - GtkWidget *menu_item = gtk_menu_item_new_with_label(mii->title); + GtkWidget *menu_item; + if (mii->isCheckable) { + menu_item = gtk_check_menu_item_new_with_label(mii->title); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menu_item), mii->checked == 1); + } else { + menu_item = gtk_menu_item_new_with_label(mii->title); + } int *id = malloc(sizeof(int)); *id = mii->menu_id; g_signal_connect_swapped(G_OBJECT(menu_item), "activate", G_CALLBACK(_systray_menu_item_selected), id); @@ -187,7 +198,7 @@ void setTooltip(char* ctooltip) { void setMenuItemIcon(const char* iconBytes, int length, int menuId, bool template) { } -void add_or_update_menu_item(int menu_id, int parent_menu_id, char* title, char* tooltip, short disabled, short checked) { +void add_or_update_menu_item(int menu_id, int parent_menu_id, char* title, char* tooltip, short disabled, short checked, short isCheckable) { // TODO: add support for sub-menus MenuItemInfo *mii = malloc(sizeof(MenuItemInfo)); mii->menu_id = menu_id; @@ -195,6 +206,7 @@ void add_or_update_menu_item(int menu_id, int parent_menu_id, char* title, char* mii->tooltip = tooltip; mii->disabled = disabled; mii->checked = checked; + mii->isCheckable = isCheckable; g_idle_add(do_add_or_update_menu_item, mii); } diff --git a/systray_nonwindows.go b/systray_nonwindows.go index c9162b5..5f6b90b 100644 --- a/systray_nonwindows.go +++ b/systray_nonwindows.go @@ -55,6 +55,10 @@ func addOrUpdateMenuItem(item *MenuItem) { if item.checked { checked = 1 } + var isCheckable C.short + if item.isCheckable { + isCheckable = 1 + } var parentID uint32 = 0 if item.parent != nil { parentID = item.parent.id @@ -66,6 +70,7 @@ func addOrUpdateMenuItem(item *MenuItem) { C.CString(item.tooltip), disabled, checked, + isCheckable, ) } From 323d5eb3e0570efbc26f1f4c1c13ca10bf8b8b4c Mon Sep 17 00:00:00 2001 From: Florian Brinker Date: Mon, 16 Nov 2020 23:22:09 +0100 Subject: [PATCH 2/2] Block event trigger during checkbox change --- systray_linux.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/systray_linux.c b/systray_linux.c index debbf68..02c5a45 100644 --- a/systray_linux.c +++ b/systray_linux.c @@ -13,6 +13,7 @@ static char temp_file_name[PATH_MAX] = ""; typedef struct { GtkWidget *menu_item; int menu_id; + long signalHandlerId; } MenuItemNode; typedef struct { @@ -95,7 +96,11 @@ gboolean do_add_or_update_menu_item(gpointer data) { gtk_menu_item_set_label(GTK_MENU_ITEM(item->menu_item), mii->title); if (mii->isCheckable) { + // We need to block the "activate" event, to emulate the same behaviour as in the windows version + // A Check/Uncheck does change the checkbox, but does not trigger the checkbox menuItem channel + g_signal_handler_block(GTK_CHECK_MENU_ITEM(item->menu_item), item->signalHandlerId); gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item->menu_item), mii->checked == 1); + g_signal_handler_unblock(GTK_CHECK_MENU_ITEM(item->menu_item), item->signalHandlerId); } break; } @@ -112,11 +117,18 @@ gboolean do_add_or_update_menu_item(gpointer data) { } int *id = malloc(sizeof(int)); *id = mii->menu_id; - g_signal_connect_swapped(G_OBJECT(menu_item), "activate", G_CALLBACK(_systray_menu_item_selected), id); + long signalHandlerId = g_signal_connect_swapped( + G_OBJECT(menu_item), + "activate", + G_CALLBACK(_systray_menu_item_selected), + id + ); + gtk_menu_shell_append(GTK_MENU_SHELL(global_tray_menu), menu_item); MenuItemNode* new_item = malloc(sizeof(MenuItemNode)); new_item->menu_id = mii->menu_id; + new_item->signalHandlerId = signalHandlerId; new_item->menu_item = menu_item; GList* new_node = malloc(sizeof(GList)); new_node->data = new_item;