diff --git a/doc/thirdparty.txt b/doc/thirdparty.txt index 9a7848d..6f85170 100644 --- a/doc/thirdparty.txt +++ b/doc/thirdparty.txt @@ -16,11 +16,6 @@ The licenses are included, and the authorship comments are left intact. - allowed uri schemes - added svg whitelist -- systray - https://github.com/getlantern/systray (commit:2c0986d) Apache 2.0 - - removed golog dependency - - fixconsole https://github.com/apenwarr/fixconsole (commit:5a9f648) Apache 2.0 diff --git a/go.mod b/go.mod index 14576d7..8fde179 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,13 @@ go 1.23.0 toolchain go1.23.5 require ( + fyne.io/systray v1.12.0 github.com/mattn/go-sqlite3 v1.14.24 golang.org/x/net v0.38.0 golang.org/x/sys v0.31.0 ) -require golang.org/x/text v0.23.0 // indirect +require ( + github.com/godbus/dbus/v5 v5.1.0 // indirect + golang.org/x/text v0.23.0 // indirect +) diff --git a/go.sum b/go.sum index a1a31c8..cade117 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +fyne.io/systray v1.12.0 h1:CA1Kk0e2zwFlxtc02L3QFSiIbxJ/P0n582YrZHT7aTM= +fyne.io/systray v1.12.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= diff --git a/src/platform/gui.go b/src/platform/gui.go index 4e2e6e7..8a27af1 100644 --- a/src/platform/gui.go +++ b/src/platform/gui.go @@ -3,8 +3,8 @@ package platform import ( + "fyne.io/systray" "github.com/nkanaev/yarr/src/server" - "github.com/nkanaev/yarr/src/systray" ) func Start(s *server.Server) { diff --git a/src/systray/NOTES.txt b/src/systray/NOTES.txt deleted file mode 100644 index e5e817f..0000000 --- a/src/systray/NOTES.txt +++ /dev/null @@ -1,12 +0,0 @@ -taken from: - -repo: -https://github.com/getlantern/systray - -hash: -2c0986dda9aea361e925f90e848d9036be7b5367 - -changes: - --removed `getlantern/golog` dependency --prevent from compiling in linux diff --git a/src/systray/README.md b/src/systray/README.md deleted file mode 100644 index 543acfd..0000000 --- a/src/systray/README.md +++ /dev/null @@ -1,120 +0,0 @@ -systray is a cross-platform Go library to place an icon and menu in the notification area. - -## Features - -* Supported on Windows, macOS, and Linux -* Menu items can be checked and/or disabled -* Methods may be called from any Goroutine - -## API - -```go -func main() { - systray.Run(onReady, onExit) -} - -func onReady() { - systray.SetIcon(icon.Data) - systray.SetTitle("Awesome App") - systray.SetTooltip("Pretty awesome超级棒") - mQuit := systray.AddMenuItem("Quit", "Quit the whole app") - - // Sets the icon of a menu item. Only available on Mac and Windows. - mQuit.SetIcon(icon.Data) -} - -func onExit() { - // clean up here -} -``` - -See [full API](https://pkg.go.dev/github.com/getlantern/systray?tab=doc) as well as [CHANGELOG](https://github.com/getlantern/systray/tree/master/CHANGELOG.md). - -## Try the example app! - -Have go v1.12+ or higher installed? Here's an example to get started on macOS: - -```sh -git clone https://github.com/getlantern/systray -cd example -env GO111MODULE=on go build -./example -``` - -On Windows, you should build like this: - -``` -env GO111MODULE=on go build -ldflags "-H=windowsgui" -``` - -The following text will then appear on the console: - - -```sh -go: finding github.com/skratchdot/open-golang latest -go: finding github.com/getlantern/systray latest -go: finding github.com/getlantern/golog latest -``` - -Now look for *Awesome App* in your menu bar! - -![Awesome App screenshot](example/screenshot.png) - -## The Webview example - -The code under `webview_example` is to demostrate how it can co-exist with other UI elements. Note that the example doesn't work on macOS versions older than 10.15 Catalina. - -## Platform notes - -### Linux - -* Building apps requires gcc as well as the `gtk3` and `libappindicator3` development headers to be installed. For Debian or Ubuntu, you may install these using: - -```sh -sudo apt-get install gcc libgtk-3-dev libappindicator3-dev -``` - -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. - -### Windows - -* To avoid opening a console at application startup, use these compile flags: - -```sh -go build -ldflags -H=windowsgui -``` - -### macOS - -On macOS, you will need to create an application bundle to wrap the binary; simply folders with the following minimal structure and assets: - -``` -SystrayApp.app/ - Contents/ - Info.plist - MacOS/ - go-executable - Resources/ - SystrayApp.icns -``` - -When running as an app bundle, you may want to add one or both of the following to your Info.plist: - -```xml - - NSHighResolutionCapable - True - - - LSUIElement - 1 -``` - -Consult the [Official Apple Documentation here](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW1). - -## Credits - -- https://github.com/xilp/systray -- https://github.com/cratonica/trayhost diff --git a/src/systray/_systray_linux.c b/src/systray/_systray_linux.c deleted file mode 100644 index 8ec3e76..0000000 --- a/src/systray/_systray_linux.c +++ /dev/null @@ -1,268 +0,0 @@ -#include -#include -#include -#include -#include -#include "systray.h" - -static AppIndicator *global_app_indicator; -static GtkWidget *global_tray_menu = NULL; -static GList *global_menu_items = NULL; -static char temp_file_name[PATH_MAX] = ""; - -typedef struct { - GtkWidget *menu_item; - int menu_id; - long signalHandlerId; -} MenuItemNode; - -typedef struct { - int menu_id; - int parent_menu_id; - char* title; - char* tooltip; - short disabled; - short checked; - short isCheckable; -} MenuItemInfo; - -void registerSystray(void) { - gtk_init(0, NULL); - global_app_indicator = app_indicator_new("systray", "", APP_INDICATOR_CATEGORY_APPLICATION_STATUS); - app_indicator_set_status(global_app_indicator, APP_INDICATOR_STATUS_ACTIVE); - global_tray_menu = gtk_menu_new(); - app_indicator_set_menu(global_app_indicator, GTK_MENU(global_tray_menu)); - systray_ready(); -} - -int nativeLoop(void) { - gtk_main(); - systray_on_exit(); - return 0; -} - -void _unlink_temp_file() { - if (strlen(temp_file_name) != 0) { - int ret = unlink(temp_file_name); - if (ret == -1) { - printf("failed to remove temp icon file %s: %s\n", temp_file_name, strerror(errno)); - } - temp_file_name[0] = '\0'; - } -} - -// runs in main thread, should always return FALSE to prevent gtk to execute it again -gboolean do_set_icon(gpointer data) { - _unlink_temp_file(); - char *tmpdir = getenv("TMPDIR"); - if (NULL == tmpdir) { - tmpdir = "/tmp"; - } - strncpy(temp_file_name, tmpdir, PATH_MAX-1); - strncat(temp_file_name, "/systray_XXXXXX", PATH_MAX-1); - temp_file_name[PATH_MAX-1] = '\0'; - - GBytes* bytes = (GBytes*)data; - int fd = mkstemp(temp_file_name); - if (fd == -1) { - printf("failed to create temp icon file %s: %s\n", temp_file_name, strerror(errno)); - return FALSE; - } - gsize size = 0; - gconstpointer icon_data = g_bytes_get_data(bytes, &size); - ssize_t written = write(fd, icon_data, size); - close(fd); - if(written != size) { - printf("failed to write temp icon file %s: %s\n", temp_file_name, strerror(errno)); - return FALSE; - } - app_indicator_set_icon_full(global_app_indicator, temp_file_name, ""); - app_indicator_set_attention_icon_full(global_app_indicator, temp_file_name, ""); - g_bytes_unref(bytes); - return FALSE; -} - -void _systray_menu_item_selected(int *id) { - systray_menu_item_selected(*id); -} - -GtkMenuItem* find_menu_by_id(int id) { - GList* it; - for(it = global_menu_items; it != NULL; it = it->next) { - MenuItemNode* item = (MenuItemNode*)(it->data); - if(item->menu_id == id) { - return GTK_MENU_ITEM(item->menu_item); - } - } - return NULL; -} - -// runs in main thread, should always return FALSE to prevent gtk to execute it again -gboolean do_add_or_update_menu_item(gpointer data) { - MenuItemInfo *mii = (MenuItemInfo*)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) { - 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; - } - } - - // menu id doesn't exist, add new item - if(it == NULL) { - 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; - long signalHandlerId = g_signal_connect_swapped( - G_OBJECT(menu_item), - "activate", - G_CALLBACK(_systray_menu_item_selected), - id - ); - - if (mii->parent_menu_id == 0) { - gtk_menu_shell_append(GTK_MENU_SHELL(global_tray_menu), menu_item); - } else { - GtkMenuItem* parentMenuItem = find_menu_by_id(mii->parent_menu_id); - GtkWidget* parentMenu = gtk_menu_item_get_submenu(parentMenuItem); - - if(parentMenu == NULL) { - parentMenu = gtk_menu_new(); - gtk_menu_item_set_submenu(parentMenuItem, parentMenu); - } - - gtk_menu_shell_append(GTK_MENU_SHELL(parentMenu), 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; - new_node->next = global_menu_items; - if(global_menu_items != NULL) { - global_menu_items->prev = new_node; - } - global_menu_items = new_node; - it = new_node; - } - GtkWidget* menu_item = GTK_WIDGET(((MenuItemNode*)(it->data))->menu_item); - gtk_widget_set_sensitive(menu_item, mii->disabled != 1); - gtk_widget_show(menu_item); - - free(mii->title); - free(mii->tooltip); - free(mii); - return FALSE; -} - -gboolean do_add_separator(gpointer data) { - GtkWidget *separator = gtk_separator_menu_item_new(); - gtk_menu_shell_append(GTK_MENU_SHELL(global_tray_menu), separator); - gtk_widget_show(separator); - return FALSE; -} - -// runs in main thread, should always return FALSE to prevent gtk to execute it again -gboolean do_hide_menu_item(gpointer data) { - MenuItemInfo *mii = (MenuItemInfo*)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){ - gtk_widget_hide(GTK_WIDGET(item->menu_item)); - break; - } - } - return FALSE; -} - -// runs in main thread, should always return FALSE to prevent gtk to execute it again -gboolean do_show_menu_item(gpointer data) { - MenuItemInfo *mii = (MenuItemInfo*)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){ - gtk_widget_show(GTK_WIDGET(item->menu_item)); - break; - } - } - return FALSE; -} - -// runs in main thread, should always return FALSE to prevent gtk to execute it again -gboolean do_quit(gpointer data) { - _unlink_temp_file(); - // app indicator doesn't provide a way to remove it, hide it as a workaround - app_indicator_set_status(global_app_indicator, APP_INDICATOR_STATUS_PASSIVE); - gtk_main_quit(); - return FALSE; -} - -void setIcon(const char* iconBytes, int length, bool template) { - GBytes* bytes = g_bytes_new_static(iconBytes, length); - g_idle_add(do_set_icon, bytes); -} - -void setTitle(char* ctitle) { - app_indicator_set_label(global_app_indicator, ctitle, ""); - free(ctitle); -} - -void setTooltip(char* ctooltip) { - free(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, short isCheckable) { - MenuItemInfo *mii = malloc(sizeof(MenuItemInfo)); - mii->menu_id = menu_id; - mii->parent_menu_id = parent_menu_id; - mii->title = title; - mii->tooltip = tooltip; - mii->disabled = disabled; - mii->checked = checked; - mii->isCheckable = isCheckable; - g_idle_add(do_add_or_update_menu_item, mii); -} - -void add_separator(int menu_id) { - MenuItemInfo *mii = malloc(sizeof(MenuItemInfo)); - mii->menu_id = menu_id; - g_idle_add(do_add_separator, mii); -} - -void hide_menu_item(int menu_id) { - MenuItemInfo *mii = malloc(sizeof(MenuItemInfo)); - mii->menu_id = menu_id; - g_idle_add(do_hide_menu_item, mii); -} - -void show_menu_item(int menu_id) { - MenuItemInfo *mii = malloc(sizeof(MenuItemInfo)); - mii->menu_id = menu_id; - g_idle_add(do_show_menu_item, mii); -} - -void quit() { - g_idle_add(do_quit, NULL); -} diff --git a/src/systray/systray_darwin.go b/src/systray/systray_darwin.go deleted file mode 100644 index 740ec5b..0000000 --- a/src/systray/systray_darwin.go +++ /dev/null @@ -1,38 +0,0 @@ -package systray - -/* -#cgo darwin CFLAGS: -DDARWIN -x objective-c -fobjc-arc -#cgo darwin LDFLAGS: -framework Cocoa -framework WebKit - -#include "systray.h" -*/ -import "C" - -import ( - "unsafe" -) - -// SetTemplateIcon sets the systray icon as a template icon (on Mac), falling back -// to a regular icon on other platforms. -// templateIconBytes and regularIconBytes should be the content of .ico for windows and -// .ico/.jpg/.png for other platforms. -func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { - cstr := (*C.char)(unsafe.Pointer(&templateIconBytes[0])) - C.setIcon(cstr, (C.int)(len(templateIconBytes)), true) -} - -// SetIcon sets the icon of a menu item. Only works on macOS and Windows. -// iconBytes should be the content of .ico/.jpg/.png -func (item *MenuItem) SetIcon(iconBytes []byte) { - cstr := (*C.char)(unsafe.Pointer(&iconBytes[0])) - C.setMenuItemIcon(cstr, (C.int)(len(iconBytes)), C.int(item.id), false) -} - -// SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows, it -// falls back to the regular icon bytes and on Linux it does nothing. -// templateIconBytes and regularIconBytes should be the content of .ico for windows and -// .ico/.jpg/.png for other platforms. -func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { - cstr := (*C.char)(unsafe.Pointer(&templateIconBytes[0])) - C.setMenuItemIcon(cstr, (C.int)(len(templateIconBytes)), C.int(item.id), true) -} diff --git a/src/systray/systray_linux.go b/src/systray/systray_linux.go deleted file mode 100644 index 2d490bd..0000000 --- a/src/systray/systray_linux.go +++ /dev/null @@ -1,32 +0,0 @@ -//go:build never -// +build never - -package systray - -/* -#cgo darwin CFLAGS: -DDARWIN -x objective-c -fobjc-arc -#cgo darwin LDFLAGS: -framework Cocoa -framework WebKit - -#include "systray.h" -*/ -import "C" - -// SetTemplateIcon sets the systray icon as a template icon (on macOS), falling back -// to a regular icon on other platforms. -// templateIconBytes and iconBytes should be the content of .ico for windows and -// .ico/.jpg/.png for other platforms. -func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { - SetIcon(regularIconBytes) -} - -// SetIcon sets the icon of a menu item. Only works on macOS and Windows. -// iconBytes should be the content of .ico/.jpg/.png -func (item *MenuItem) SetIcon(iconBytes []byte) { -} - -// SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows, it -// falls back to the regular icon bytes and on Linux it does nothing. -// templateIconBytes and regularIconBytes should be the content of .ico for windows and -// .ico/.jpg/.png for other platforms. -func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { -} diff --git a/src/systray/systray_nonwindows.go b/src/systray/systray_nonwindows.go deleted file mode 100644 index e17358a..0000000 --- a/src/systray/systray_nonwindows.go +++ /dev/null @@ -1,106 +0,0 @@ -//go:build darwin -// +build darwin - -package systray - -/* -#cgo darwin CFLAGS: -DDARWIN -x objective-c -fobjc-arc -#cgo darwin LDFLAGS: -framework Cocoa - -#include "systray.h" -*/ -import "C" - -import ( - "unsafe" -) - -func registerSystray() { - C.registerSystray() -} - -func nativeLoop() { - C.nativeLoop() -} - -func quit() { - C.quit() -} - -// SetIcon sets the systray icon. -// iconBytes should be the content of .ico for windows and .ico/.jpg/.png -// for other platforms. -func SetIcon(iconBytes []byte) { - cstr := (*C.char)(unsafe.Pointer(&iconBytes[0])) - C.setIcon(cstr, (C.int)(len(iconBytes)), false) -} - -// SetTitle sets the systray title, only available on Mac and Linux. -func SetTitle(title string) { - C.setTitle(C.CString(title)) -} - -// SetTooltip sets the systray tooltip to display on mouse hover of the tray icon, -// only available on Mac and Windows. -func SetTooltip(tooltip string) { - C.setTooltip(C.CString(tooltip)) -} - -func addOrUpdateMenuItem(item *MenuItem) { - var disabled C.short - if item.disabled { - disabled = 1 - } - var checked C.short - 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 - } - C.add_or_update_menu_item( - C.int(item.id), - C.int(parentID), - C.CString(item.title), - C.CString(item.tooltip), - disabled, - checked, - isCheckable, - ) -} - -func addSeparator(id uint32) { - C.add_separator(C.int(id)) -} - -func hideMenuItem(item *MenuItem) { - C.hide_menu_item( - C.int(item.id), - ) -} - -func showMenuItem(item *MenuItem) { - C.show_menu_item( - C.int(item.id), - ) -} - -//export systray_ready -func systray_ready() { - systrayReady() -} - -//export systray_on_exit -func systray_on_exit() { - systrayExit() -} - -//export systray_menu_item_selected -func systray_menu_item_selected(cID C.int) { - systrayMenuItemSelected(uint32(cID)) -} diff --git a/src/systray/systray_windows_test.go b/src/systray/systray_windows_test.go deleted file mode 100644 index ca2a9c9..0000000 --- a/src/systray/systray_windows_test.go +++ /dev/null @@ -1,133 +0,0 @@ -//go:build windows -// +build windows - -package systray - -import ( - "io/ioutil" - "runtime" - "sync/atomic" - "testing" - "time" - "unsafe" - - "golang.org/x/sys/windows" -) - -const iconFilePath = "example/icon/iconwin.ico" - -func TestBaseWindowsTray(t *testing.T) { - systrayReady = func() {} - systrayExit = func() {} - - runtime.LockOSThread() - - if err := wt.initInstance(); err != nil { - t.Fatalf("initInstance failed: %s", err) - } - - if err := wt.createMenu(); err != nil { - t.Fatalf("createMenu failed: %s", err) - } - - defer func() { - pDestroyWindow.Call(uintptr(wt.window)) - wt.wcex.unregister() - }() - - if err := wt.setIcon(iconFilePath); err != nil { - t.Errorf("SetIcon failed: %s", err) - } - - if err := wt.setTooltip("Cyrillic tooltip тест:)"); err != nil { - t.Errorf("SetIcon failed: %s", err) - } - - var id int32 = 0 - err := wt.addOrUpdateMenuItem(atomic.AddInt32(&id, 1), "Simple enabled", false, false) - if err != nil { - t.Errorf("mergeMenuItem failed: %s", err) - } - err = wt.addOrUpdateMenuItem(atomic.AddInt32(&id, 1), "Simple disabled", true, false) - if err != nil { - t.Errorf("mergeMenuItem failed: %s", err) - } - err = wt.addSeparatorMenuItem(atomic.AddInt32(&id, 1)) - if err != nil { - t.Errorf("addSeparatorMenuItem failed: %s", err) - } - err = wt.addOrUpdateMenuItem(atomic.AddInt32(&id, 1), "Simple checked enabled", false, true) - if err != nil { - t.Errorf("mergeMenuItem failed: %s", err) - } - err = wt.addOrUpdateMenuItem(atomic.AddInt32(&id, 1), "Simple checked disabled", true, true) - if err != nil { - t.Errorf("mergeMenuItem failed: %s", err) - } - - err = wt.hideMenuItem(1) - if err != nil { - t.Errorf("hideMenuItem failed: %s", err) - } - - err = wt.hideMenuItem(100) - if err == nil { - t.Error("hideMenuItem failed: must return error on invalid item id") - } - - err = wt.addOrUpdateMenuItem(2, "Simple disabled update", true, false) - if err != nil { - t.Errorf("mergeMenuItem failed: %s", err) - } - - time.AfterFunc(1*time.Second, quit) - - m := struct { - WindowHandle windows.Handle - Message uint32 - Wparam uintptr - Lparam uintptr - Time uint32 - Pt point - }{} - for { - ret, _, err := pGetMessage.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0) - res := int32(ret) - if res == -1 { - t.Errorf("win32 GetMessage failed: %v", err) - return - } else if res == 0 { - break - } - pTranslateMessage.Call(uintptr(unsafe.Pointer(&m))) - pDispatchMessage.Call(uintptr(unsafe.Pointer(&m))) - } -} - -func TestWindowsRun(t *testing.T) { - onReady := func() { - b, err := ioutil.ReadFile(iconFilePath) - if err != nil { - t.Fatalf("Can't load icon file: %v", err) - } - SetIcon(b) - SetTitle("Test title с кириллицей") - - bSomeBtn := AddMenuItem("Йа кнопко", "") - bSomeBtn.Check() - AddSeparator() - bQuit := AddMenuItem("Quit", "Quit the whole app") - go func() { - <-bQuit.ClickedCh - t.Log("Quit reqested") - Quit() - }() - time.AfterFunc(1*time.Second, Quit) - } - - onExit := func() { - t.Log("Exit success") - } - - Run(onReady, onExit) -} diff --git a/src/systray/.gitignore b/vendor/fyne.io/systray/.gitignore similarity index 95% rename from src/systray/.gitignore rename to vendor/fyne.io/systray/.gitignore index 2e85ef6..2ff640b 100644 --- a/src/systray/.gitignore +++ b/vendor/fyne.io/systray/.gitignore @@ -10,3 +10,4 @@ dll/systray_unsigned.dll out.txt .vs on_exit*.txt +.vscode \ No newline at end of file diff --git a/src/systray/CHANGELOG.md b/vendor/fyne.io/systray/CHANGELOG.md similarity index 100% rename from src/systray/CHANGELOG.md rename to vendor/fyne.io/systray/CHANGELOG.md diff --git a/src/systray/LICENSE b/vendor/fyne.io/systray/LICENSE similarity index 100% rename from src/systray/LICENSE rename to vendor/fyne.io/systray/LICENSE diff --git a/src/systray/Makefile b/vendor/fyne.io/systray/Makefile similarity index 100% rename from src/systray/Makefile rename to vendor/fyne.io/systray/Makefile diff --git a/vendor/fyne.io/systray/README.md b/vendor/fyne.io/systray/README.md new file mode 100644 index 0000000..6881e95 --- /dev/null +++ b/vendor/fyne.io/systray/README.md @@ -0,0 +1,147 @@ +# Systray + +systray is a cross-platform Go library to place an icon and menu in the notification area. +This repository is a fork of [getlantern/systray](https://github.com/getlantern/systray) +removing the GTK dependency and support for legacy linux system tray. + +## Features + +* Supported on Windows, macOS, Linux and many BSD systems +* Menu items can be checked and/or disabled +* Methods may be called from any Goroutine + +## API + +```go +package main + +import "fyne.io/systray" +import "fyne.io/systray/example/icon" + +func main() { + systray.Run(onReady, onExit) +} + +func onReady() { + systray.SetIcon(icon.Data) + systray.SetTitle("Awesome App") + systray.SetTooltip("Pretty awesome超级棒") + mQuit := systray.AddMenuItem("Quit", "Quit the whole app") + + // Sets the icon of a menu item. + mQuit.SetIcon(icon.Data) +} + +func onExit() { + // clean up here +} +``` + +### Running in a Fyne app + +This repository is designed to allow any toolkit to integrate system tray without any additional dependencies. +It is maintained by the Fyne team, but if you are using Fyne there is an even easier to use API in the main repository that wraps this project. + +In your app you can use a standard `fyne.Menu` structure and pass it to `SetSystemTrayMenu` when your app is a desktop app, as follows: + +```go + menu := fyne.NewMenu("MyApp", + fyne.NewMenuItem("Show", func() { + log.Println("Tapped show") + })) + + if desk, ok := myApp.(desktop.App); ok { + desk.SetSystemTrayMenu(menu) + } +``` + +You can find out more in the toolkit documentation: +[System Tray Menu](https://developer.fyne.io/explore/systray). + +### Run in another toolkit + +Most graphical toolkits will grab the main loop so the `Run` code above is not possible. +For this reason there is another entry point `RunWithExternalLoop`. +This function of the library returns a start and end function that should be called +when the application has started and will end, to loop in appropriate features. + +See [full API](https://pkg.go.dev/fyne.io/systray?tab=doc) as well as [CHANGELOG](https://github.com/fyne-io/systray/tree/master/CHANGELOG.md). + +Note: this package requires cgo, so make sure you set `CGO_ENABLED=1` before building. + +## Try the example app! + +Have go v1.12+ or higher installed? Here's an example to get started on macOS or Linux: + +```sh +git clone https://github.com/fyne-io/systray +cd systray/example +go run . +``` + +On Windows, you should follow the instructions above, but use the followign run command: + +``` +go run -ldflags "-H=windowsgui" . +``` + +Now look for *Awesome App* in your menu bar! + +![Awesome App screenshot](example/screenshot.png) + +## Platform notes + +### Linux/BSD + +This implementation uses DBus to communicate through the SystemNotifier/AppIndicator spec, older tray implementations may not load the icon. + +If you are running an older desktop environment, or system tray provider, you may require a proxy app which can convert the new DBus calls to the old format. +The recommended tool for Gnome based trays is [snixembed](https://git.sr.ht/~steef/snixembed), others are available. +Search for "StatusNotifierItems XEmbedded" in your package manager. + +### Windows + +* To avoid opening a console at application startup, use "fyne package" for your app or manually use these compile flags: + +```sh +go build -ldflags -H=windowsgui +``` + +### macOS + +On macOS, you will need to create an application bundle to wrap the binary; simply use "fyne package" or add folders with the following minimal structure and assets: + +``` +SystrayApp.app/ + Contents/ + Info.plist + MacOS/ + go-executable + Resources/ + SystrayApp.icns +``` + +If bundling manually, you may want to add one or both of the following to your Info.plist: + +```xml + + NSHighResolutionCapable + True + + + LSUIElement + 1 +``` + +Consult the [Official Apple Documentation here](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW1). + +On macOS, it's possible to set the underlying +[`NSStatusItemBehavior`](https://developer.apple.com/documentation/appkit/nsstatusitembehavior?language=objc) +with `systray.SetRemovalAllowed(true)`. When enabled, the user can cmd-drag the +icon off the menu bar. + +## Credits + +- https://github.com/getlantern/systray +- https://github.com/xilp/systray +- https://github.com/cratonica/trayhost diff --git a/vendor/fyne.io/systray/internal/generated/menu/dbus_menu.go b/vendor/fyne.io/systray/internal/generated/menu/dbus_menu.go new file mode 100644 index 0000000..1b896d3 --- /dev/null +++ b/vendor/fyne.io/systray/internal/generated/menu/dbus_menu.go @@ -0,0 +1,484 @@ +// Code generated by dbus-codegen-go DO NOT EDIT. +package menu + +import ( + "context" + "errors" + "fmt" + + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" +) + +var ( + // Introspection for com.canonical.dbusmenu + IntrospectDataDbusmenu = introspect.Interface{ + Name: "com.canonical.dbusmenu", + Methods: []introspect.Method{{Name: "GetLayout", Args: []introspect.Arg{ + {Name: "parentId", Type: "i", Direction: "in"}, + {Name: "recursionDepth", Type: "i", Direction: "in"}, + {Name: "propertyNames", Type: "as", Direction: "in"}, + {Name: "revision", Type: "u", Direction: "out"}, + {Name: "layout", Type: "(ia{sv}av)", Direction: "out"}, + }}, + {Name: "GetGroupProperties", Args: []introspect.Arg{ + {Name: "ids", Type: "ai", Direction: "in"}, + {Name: "propertyNames", Type: "as", Direction: "in"}, + {Name: "properties", Type: "a(ia{sv})", Direction: "out"}, + }}, + {Name: "GetProperty", Args: []introspect.Arg{ + {Name: "id", Type: "i", Direction: "in"}, + {Name: "name", Type: "s", Direction: "in"}, + {Name: "value", Type: "v", Direction: "out"}, + }}, + {Name: "Event", Args: []introspect.Arg{ + {Name: "id", Type: "i", Direction: "in"}, + {Name: "eventId", Type: "s", Direction: "in"}, + {Name: "data", Type: "v", Direction: "in"}, + {Name: "timestamp", Type: "u", Direction: "in"}, + }}, + {Name: "EventGroup", Args: []introspect.Arg{ + {Name: "events", Type: "a(isvu)", Direction: "in"}, + {Name: "idErrors", Type: "ai", Direction: "out"}, + }}, + {Name: "AboutToShow", Args: []introspect.Arg{ + {Name: "id", Type: "i", Direction: "in"}, + {Name: "needUpdate", Type: "b", Direction: "out"}, + }}, + {Name: "AboutToShowGroup", Args: []introspect.Arg{ + {Name: "ids", Type: "ai", Direction: "in"}, + {Name: "updatesNeeded", Type: "ai", Direction: "out"}, + {Name: "idErrors", Type: "ai", Direction: "out"}, + }}, + }, + Signals: []introspect.Signal{{Name: "ItemsPropertiesUpdated", Args: []introspect.Arg{ + {Name: "updatedProps", Type: "a(ia{sv})", Direction: "out"}, + {Name: "removedProps", Type: "a(ias)", Direction: "out"}, + }}, + {Name: "LayoutUpdated", Args: []introspect.Arg{ + {Name: "revision", Type: "u", Direction: "out"}, + {Name: "parent", Type: "i", Direction: "out"}, + }}, + {Name: "ItemActivationRequested", Args: []introspect.Arg{ + {Name: "id", Type: "i", Direction: "out"}, + {Name: "timestamp", Type: "u", Direction: "out"}, + }}, + }, + Properties: []introspect.Property{{Name: "Version", Type: "u", Access: "read"}, + {Name: "TextDirection", Type: "s", Access: "read"}, + {Name: "Status", Type: "s", Access: "read"}, + {Name: "IconThemePath", Type: "as", Access: "read"}, + }, + Annotations: []introspect.Annotation{}, + } +) + +// Signal is a common interface for all signals. +type Signal interface { + Name() string + Interface() string + Sender() string + + path() dbus.ObjectPath + values() []interface{} +} + +// Emit sends the given signal to the bus. +func Emit(conn *dbus.Conn, s Signal) error { + return conn.Emit(s.path(), s.Interface()+"."+s.Name(), s.values()...) +} + +// ErrUnknownSignal is returned by LookupSignal when a signal cannot be resolved. +var ErrUnknownSignal = errors.New("unknown signal") + +// LookupSignal converts the given raw D-Bus signal with variable body +// into one with typed structured body or returns ErrUnknownSignal error. +func LookupSignal(signal *dbus.Signal) (Signal, error) { + switch signal.Name { + case InterfaceDbusmenu + "." + "ItemsPropertiesUpdated": + v0, ok := signal.Body[0].([]struct { + V0 int32 + V1 map[string]dbus.Variant + }) + if !ok { + return nil, fmt.Errorf("prop .UpdatedProps is %T, not []struct {V0 int32;V1 map[string]dbus.Variant}", signal.Body[0]) + } + v1, ok := signal.Body[1].([]struct { + V0 int32 + V1 []string + }) + if !ok { + return nil, fmt.Errorf("prop .RemovedProps is %T, not []struct {V0 int32;V1 []string}", signal.Body[1]) + } + return &Dbusmenu_ItemsPropertiesUpdatedSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &Dbusmenu_ItemsPropertiesUpdatedSignalBody{ + UpdatedProps: v0, + RemovedProps: v1, + }, + }, nil + case InterfaceDbusmenu + "." + "LayoutUpdated": + v0, ok := signal.Body[0].(uint32) + if !ok { + return nil, fmt.Errorf("prop .Revision is %T, not uint32", signal.Body[0]) + } + v1, ok := signal.Body[1].(int32) + if !ok { + return nil, fmt.Errorf("prop .Parent is %T, not int32", signal.Body[1]) + } + return &Dbusmenu_LayoutUpdatedSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &Dbusmenu_LayoutUpdatedSignalBody{ + Revision: v0, + Parent: v1, + }, + }, nil + case InterfaceDbusmenu + "." + "ItemActivationRequested": + v0, ok := signal.Body[0].(int32) + if !ok { + return nil, fmt.Errorf("prop .Id is %T, not int32", signal.Body[0]) + } + v1, ok := signal.Body[1].(uint32) + if !ok { + return nil, fmt.Errorf("prop .Timestamp is %T, not uint32", signal.Body[1]) + } + return &Dbusmenu_ItemActivationRequestedSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &Dbusmenu_ItemActivationRequestedSignalBody{ + Id: v0, + Timestamp: v1, + }, + }, nil + default: + return nil, ErrUnknownSignal + } +} + +// AddMatchSignal registers a match rule for the given signal, +// opts are appended to the automatically generated signal's rules. +func AddMatchSignal(conn *dbus.Conn, s Signal, opts ...dbus.MatchOption) error { + return conn.AddMatchSignal(append([]dbus.MatchOption{ + dbus.WithMatchInterface(s.Interface()), + dbus.WithMatchMember(s.Name()), + }, opts...)...) +} + +// RemoveMatchSignal unregisters the previously registered subscription. +func RemoveMatchSignal(conn *dbus.Conn, s Signal, opts ...dbus.MatchOption) error { + return conn.RemoveMatchSignal(append([]dbus.MatchOption{ + dbus.WithMatchInterface(s.Interface()), + dbus.WithMatchMember(s.Name()), + }, opts...)...) +} + +// Interface name constants. +const ( + InterfaceDbusmenu = "com.canonical.dbusmenu" +) + +// Dbusmenuer is com.canonical.dbusmenu interface. +type Dbusmenuer interface { + // GetLayout is com.canonical.dbusmenu.GetLayout method. + GetLayout(parentId int32, recursionDepth int32, propertyNames []string) (revision uint32, layout struct { + V0 int32 + V1 map[string]dbus.Variant + V2 []dbus.Variant + }, err *dbus.Error) + // GetGroupProperties is com.canonical.dbusmenu.GetGroupProperties method. + GetGroupProperties(ids []int32, propertyNames []string) (properties []struct { + V0 int32 + V1 map[string]dbus.Variant + }, err *dbus.Error) + // GetProperty is com.canonical.dbusmenu.GetProperty method. + GetProperty(id int32, name string) (value dbus.Variant, err *dbus.Error) + // Event is com.canonical.dbusmenu.Event method. + Event(id int32, eventId string, data dbus.Variant, timestamp uint32) (err *dbus.Error) + // EventGroup is com.canonical.dbusmenu.EventGroup method. + EventGroup(events []struct { + V0 int32 + V1 string + V2 dbus.Variant + V3 uint32 + }) (idErrors []int32, err *dbus.Error) + // AboutToShow is com.canonical.dbusmenu.AboutToShow method. + AboutToShow(id int32) (needUpdate bool, err *dbus.Error) + // AboutToShowGroup is com.canonical.dbusmenu.AboutToShowGroup method. + AboutToShowGroup(ids []int32) (updatesNeeded []int32, idErrors []int32, err *dbus.Error) +} + +// ExportDbusmenu exports the given object that implements com.canonical.dbusmenu on the bus. +func ExportDbusmenu(conn *dbus.Conn, path dbus.ObjectPath, v Dbusmenuer) error { + return conn.ExportSubtreeMethodTable(map[string]interface{}{ + "GetLayout": v.GetLayout, + "GetGroupProperties": v.GetGroupProperties, + "GetProperty": v.GetProperty, + "Event": v.Event, + "EventGroup": v.EventGroup, + "AboutToShow": v.AboutToShow, + "AboutToShowGroup": v.AboutToShowGroup, + }, path, InterfaceDbusmenu) +} + +// UnexportDbusmenu unexports com.canonical.dbusmenu interface on the named path. +func UnexportDbusmenu(conn *dbus.Conn, path dbus.ObjectPath) error { + return conn.Export(nil, path, InterfaceDbusmenu) +} + +// UnimplementedDbusmenu can be embedded to have forward compatible server implementations. +type UnimplementedDbusmenu struct{} + +func (*UnimplementedDbusmenu) iface() string { + return InterfaceDbusmenu +} + +func (*UnimplementedDbusmenu) GetLayout(parentId int32, recursionDepth int32, propertyNames []string) (revision uint32, layout struct { + V0 int32 + V1 map[string]dbus.Variant + V2 []dbus.Variant +}, err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*UnimplementedDbusmenu) GetGroupProperties(ids []int32, propertyNames []string) (properties []struct { + V0 int32 + V1 map[string]dbus.Variant +}, err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*UnimplementedDbusmenu) GetProperty(id int32, name string) (value dbus.Variant, err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*UnimplementedDbusmenu) Event(id int32, eventId string, data dbus.Variant, timestamp uint32) (err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*UnimplementedDbusmenu) EventGroup(events []struct { + V0 int32 + V1 string + V2 dbus.Variant + V3 uint32 +}) (idErrors []int32, err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*UnimplementedDbusmenu) AboutToShow(id int32) (needUpdate bool, err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*UnimplementedDbusmenu) AboutToShowGroup(ids []int32) (updatesNeeded []int32, idErrors []int32, err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +// NewDbusmenu creates and allocates com.canonical.dbusmenu. +func NewDbusmenu(object dbus.BusObject) *Dbusmenu { + return &Dbusmenu{object} +} + +// Dbusmenu implements com.canonical.dbusmenu D-Bus interface. +type Dbusmenu struct { + object dbus.BusObject +} + +// GetLayout calls com.canonical.dbusmenu.GetLayout method. +func (o *Dbusmenu) GetLayout(ctx context.Context, parentId int32, recursionDepth int32, propertyNames []string) (revision uint32, layout struct { + V0 int32 + V1 map[string]dbus.Variant + V2 []dbus.Variant +}, err error) { + err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".GetLayout", 0, parentId, recursionDepth, propertyNames).Store(&revision, &layout) + return +} + +// GetGroupProperties calls com.canonical.dbusmenu.GetGroupProperties method. +func (o *Dbusmenu) GetGroupProperties(ctx context.Context, ids []int32, propertyNames []string) (properties []struct { + V0 int32 + V1 map[string]dbus.Variant +}, err error) { + err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".GetGroupProperties", 0, ids, propertyNames).Store(&properties) + return +} + +// GetProperty calls com.canonical.dbusmenu.GetProperty method. +func (o *Dbusmenu) GetProperty(ctx context.Context, id int32, name string) (value dbus.Variant, err error) { + err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".GetProperty", 0, id, name).Store(&value) + return +} + +// Event calls com.canonical.dbusmenu.Event method. +func (o *Dbusmenu) Event(ctx context.Context, id int32, eventId string, data dbus.Variant, timestamp uint32) (err error) { + err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".Event", 0, id, eventId, data, timestamp).Store() + return +} + +// EventGroup calls com.canonical.dbusmenu.EventGroup method. +func (o *Dbusmenu) EventGroup(ctx context.Context, events []struct { + V0 int32 + V1 string + V2 dbus.Variant + V3 uint32 +}) (idErrors []int32, err error) { + err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".EventGroup", 0, events).Store(&idErrors) + return +} + +// AboutToShow calls com.canonical.dbusmenu.AboutToShow method. +func (o *Dbusmenu) AboutToShow(ctx context.Context, id int32) (needUpdate bool, err error) { + err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".AboutToShow", 0, id).Store(&needUpdate) + return +} + +// AboutToShowGroup calls com.canonical.dbusmenu.AboutToShowGroup method. +func (o *Dbusmenu) AboutToShowGroup(ctx context.Context, ids []int32) (updatesNeeded []int32, idErrors []int32, err error) { + err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".AboutToShowGroup", 0, ids).Store(&updatesNeeded, &idErrors) + return +} + +// GetVersion gets com.canonical.dbusmenu.Version property. +func (o *Dbusmenu) GetVersion(ctx context.Context) (version uint32, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceDbusmenu, "Version").Store(&version) + return +} + +// GetTextDirection gets com.canonical.dbusmenu.TextDirection property. +func (o *Dbusmenu) GetTextDirection(ctx context.Context) (textDirection string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceDbusmenu, "TextDirection").Store(&textDirection) + return +} + +// GetStatus gets com.canonical.dbusmenu.Status property. +func (o *Dbusmenu) GetStatus(ctx context.Context) (status string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceDbusmenu, "Status").Store(&status) + return +} + +// GetIconThemePath gets com.canonical.dbusmenu.IconThemePath property. +func (o *Dbusmenu) GetIconThemePath(ctx context.Context) (iconThemePath []string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceDbusmenu, "IconThemePath").Store(&iconThemePath) + return +} + +// Dbusmenu_ItemsPropertiesUpdatedSignal represents com.canonical.dbusmenu.ItemsPropertiesUpdated signal. +type Dbusmenu_ItemsPropertiesUpdatedSignal struct { + sender string + Path dbus.ObjectPath + Body *Dbusmenu_ItemsPropertiesUpdatedSignalBody +} + +// Name returns the signal's name. +func (s *Dbusmenu_ItemsPropertiesUpdatedSignal) Name() string { + return "ItemsPropertiesUpdated" +} + +// Interface returns the signal's interface. +func (s *Dbusmenu_ItemsPropertiesUpdatedSignal) Interface() string { + return InterfaceDbusmenu +} + +// Sender returns the signal's sender unique name. +func (s *Dbusmenu_ItemsPropertiesUpdatedSignal) Sender() string { + return s.sender +} + +func (s *Dbusmenu_ItemsPropertiesUpdatedSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *Dbusmenu_ItemsPropertiesUpdatedSignal) values() []interface{} { + return []interface{}{s.Body.UpdatedProps, s.Body.RemovedProps} +} + +// Dbusmenu_ItemsPropertiesUpdatedSignalBody is body container. +type Dbusmenu_ItemsPropertiesUpdatedSignalBody struct { + UpdatedProps []struct { + V0 int32 + V1 map[string]dbus.Variant + } + RemovedProps []struct { + V0 int32 + V1 []string + } +} + +// Dbusmenu_LayoutUpdatedSignal represents com.canonical.dbusmenu.LayoutUpdated signal. +type Dbusmenu_LayoutUpdatedSignal struct { + sender string + Path dbus.ObjectPath + Body *Dbusmenu_LayoutUpdatedSignalBody +} + +// Name returns the signal's name. +func (s *Dbusmenu_LayoutUpdatedSignal) Name() string { + return "LayoutUpdated" +} + +// Interface returns the signal's interface. +func (s *Dbusmenu_LayoutUpdatedSignal) Interface() string { + return InterfaceDbusmenu +} + +// Sender returns the signal's sender unique name. +func (s *Dbusmenu_LayoutUpdatedSignal) Sender() string { + return s.sender +} + +func (s *Dbusmenu_LayoutUpdatedSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *Dbusmenu_LayoutUpdatedSignal) values() []interface{} { + return []interface{}{s.Body.Revision, s.Body.Parent} +} + +// Dbusmenu_LayoutUpdatedSignalBody is body container. +type Dbusmenu_LayoutUpdatedSignalBody struct { + Revision uint32 + Parent int32 +} + +// Dbusmenu_ItemActivationRequestedSignal represents com.canonical.dbusmenu.ItemActivationRequested signal. +type Dbusmenu_ItemActivationRequestedSignal struct { + sender string + Path dbus.ObjectPath + Body *Dbusmenu_ItemActivationRequestedSignalBody +} + +// Name returns the signal's name. +func (s *Dbusmenu_ItemActivationRequestedSignal) Name() string { + return "ItemActivationRequested" +} + +// Interface returns the signal's interface. +func (s *Dbusmenu_ItemActivationRequestedSignal) Interface() string { + return InterfaceDbusmenu +} + +// Sender returns the signal's sender unique name. +func (s *Dbusmenu_ItemActivationRequestedSignal) Sender() string { + return s.sender +} + +func (s *Dbusmenu_ItemActivationRequestedSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *Dbusmenu_ItemActivationRequestedSignal) values() []interface{} { + return []interface{}{s.Body.Id, s.Body.Timestamp} +} + +// Dbusmenu_ItemActivationRequestedSignalBody is body container. +type Dbusmenu_ItemActivationRequestedSignalBody struct { + Id int32 + Timestamp uint32 +} diff --git a/vendor/fyne.io/systray/internal/generated/notifier/status_notifier_item.go b/vendor/fyne.io/systray/internal/generated/notifier/status_notifier_item.go new file mode 100644 index 0000000..230c40d --- /dev/null +++ b/vendor/fyne.io/systray/internal/generated/notifier/status_notifier_item.go @@ -0,0 +1,633 @@ +// Code generated by dbus-codegen-go DO NOT EDIT. +package notifier + +import ( + "context" + "errors" + "fmt" + + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" +) + +var ( + // Introspection for org.kde.StatusNotifierItem + IntrospectDataStatusNotifierItem = introspect.Interface{ + Name: "org.kde.StatusNotifierItem", + Methods: []introspect.Method{{Name: "ContextMenu", Args: []introspect.Arg{ + {Name: "x", Type: "i", Direction: "in"}, + {Name: "y", Type: "i", Direction: "in"}, + }}, + {Name: "Activate", Args: []introspect.Arg{ + {Name: "x", Type: "i", Direction: "in"}, + {Name: "y", Type: "i", Direction: "in"}, + }}, + {Name: "SecondaryActivate", Args: []introspect.Arg{ + {Name: "x", Type: "i", Direction: "in"}, + {Name: "y", Type: "i", Direction: "in"}, + }}, + {Name: "Scroll", Args: []introspect.Arg{ + {Name: "delta", Type: "i", Direction: "in"}, + {Name: "orientation", Type: "s", Direction: "in"}, + }}, + }, + Signals: []introspect.Signal{{Name: "NewTitle"}, + {Name: "NewIcon"}, + {Name: "NewAttentionIcon"}, + {Name: "NewOverlayIcon"}, + {Name: "NewStatus", Args: []introspect.Arg{ + {Name: "status", Type: "s", Direction: ""}, + }}, + {Name: "NewIconThemePath", Args: []introspect.Arg{ + {Name: "icon_theme_path", Type: "s", Direction: "out"}, + }}, + {Name: "NewMenu"}, + }, + Properties: []introspect.Property{{Name: "Category", Type: "s", Access: "read"}, + {Name: "Id", Type: "s", Access: "read"}, + {Name: "Title", Type: "s", Access: "read"}, + {Name: "Status", Type: "s", Access: "read"}, + {Name: "WindowId", Type: "i", Access: "read"}, + {Name: "IconThemePath", Type: "s", Access: "read"}, + {Name: "Menu", Type: "o", Access: "read"}, + {Name: "ItemIsMenu", Type: "b", Access: "read"}, + {Name: "IconName", Type: "s", Access: "read"}, + {Name: "IconPixmap", Type: "a(iiay)", Access: "read", Annotations: []introspect.Annotation{ + {Name: "org.qtproject.QtDBus.QtTypeName", Value: "KDbusImageVector"}, + }}, + {Name: "OverlayIconName", Type: "s", Access: "read"}, + {Name: "OverlayIconPixmap", Type: "a(iiay)", Access: "read", Annotations: []introspect.Annotation{ + {Name: "org.qtproject.QtDBus.QtTypeName", Value: "KDbusImageVector"}, + }}, + {Name: "AttentionIconName", Type: "s", Access: "read"}, + {Name: "AttentionIconPixmap", Type: "a(iiay)", Access: "read", Annotations: []introspect.Annotation{ + {Name: "org.qtproject.QtDBus.QtTypeName", Value: "KDbusImageVector"}, + }}, + {Name: "AttentionMovieName", Type: "s", Access: "read"}, + {Name: "ToolTip", Type: "(sa(iiay)ss)", Access: "read", Annotations: []introspect.Annotation{ + {Name: "org.qtproject.QtDBus.QtTypeName", Value: "KDbusToolTipStruct"}, + }}, + }, + Annotations: []introspect.Annotation{}, + } +) + +// Signal is a common interface for all signals. +type Signal interface { + Name() string + Interface() string + Sender() string + + path() dbus.ObjectPath + values() []interface{} +} + +// Emit sends the given signal to the bus. +func Emit(conn *dbus.Conn, s Signal) error { + return conn.Emit(s.path(), s.Interface()+"."+s.Name(), s.values()...) +} + +// ErrUnknownSignal is returned by LookupSignal when a signal cannot be resolved. +var ErrUnknownSignal = errors.New("unknown signal") + +// LookupSignal converts the given raw D-Bus signal with variable body +// into one with typed structured body or returns ErrUnknownSignal error. +func LookupSignal(signal *dbus.Signal) (Signal, error) { + switch signal.Name { + case InterfaceStatusNotifierItem + "." + "NewTitle": + return &StatusNotifierItem_NewTitleSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &StatusNotifierItem_NewTitleSignalBody{}, + }, nil + case InterfaceStatusNotifierItem + "." + "NewIcon": + return &StatusNotifierItem_NewIconSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &StatusNotifierItem_NewIconSignalBody{}, + }, nil + case InterfaceStatusNotifierItem + "." + "NewAttentionIcon": + return &StatusNotifierItem_NewAttentionIconSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &StatusNotifierItem_NewAttentionIconSignalBody{}, + }, nil + case InterfaceStatusNotifierItem + "." + "NewOverlayIcon": + return &StatusNotifierItem_NewOverlayIconSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &StatusNotifierItem_NewOverlayIconSignalBody{}, + }, nil + case InterfaceStatusNotifierItem + "." + "NewStatus": + v0, ok := signal.Body[0].(string) + if !ok { + return nil, fmt.Errorf("prop .Status is %T, not string", signal.Body[0]) + } + return &StatusNotifierItem_NewStatusSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &StatusNotifierItem_NewStatusSignalBody{ + Status: v0, + }, + }, nil + case InterfaceStatusNotifierItem + "." + "NewIconThemePath": + v0, ok := signal.Body[0].(string) + if !ok { + return nil, fmt.Errorf("prop .IconThemePath is %T, not string", signal.Body[0]) + } + return &StatusNotifierItem_NewIconThemePathSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &StatusNotifierItem_NewIconThemePathSignalBody{ + IconThemePath: v0, + }, + }, nil + case InterfaceStatusNotifierItem + "." + "NewMenu": + return &StatusNotifierItem_NewMenuSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &StatusNotifierItem_NewMenuSignalBody{}, + }, nil + default: + return nil, ErrUnknownSignal + } +} + +// AddMatchSignal registers a match rule for the given signal, +// opts are appended to the automatically generated signal's rules. +func AddMatchSignal(conn *dbus.Conn, s Signal, opts ...dbus.MatchOption) error { + return conn.AddMatchSignal(append([]dbus.MatchOption{ + dbus.WithMatchInterface(s.Interface()), + dbus.WithMatchMember(s.Name()), + }, opts...)...) +} + +// RemoveMatchSignal unregisters the previously registered subscription. +func RemoveMatchSignal(conn *dbus.Conn, s Signal, opts ...dbus.MatchOption) error { + return conn.RemoveMatchSignal(append([]dbus.MatchOption{ + dbus.WithMatchInterface(s.Interface()), + dbus.WithMatchMember(s.Name()), + }, opts...)...) +} + +// Interface name constants. +const ( + InterfaceStatusNotifierItem = "org.kde.StatusNotifierItem" +) + +// StatusNotifierItemer is org.kde.StatusNotifierItem interface. +type StatusNotifierItemer interface { + // ContextMenu is org.kde.StatusNotifierItem.ContextMenu method. + ContextMenu(x int32, y int32) (err *dbus.Error) + // Activate is org.kde.StatusNotifierItem.Activate method. + Activate(x int32, y int32) (err *dbus.Error) + // SecondaryActivate is org.kde.StatusNotifierItem.SecondaryActivate method. + SecondaryActivate(x int32, y int32) (err *dbus.Error) + // Scroll is org.kde.StatusNotifierItem.Scroll method. + Scroll(delta int32, orientation string) (err *dbus.Error) +} + +// ExportStatusNotifierItem exports the given object that implements org.kde.StatusNotifierItem on the bus. +func ExportStatusNotifierItem(conn *dbus.Conn, path dbus.ObjectPath, v StatusNotifierItemer) error { + return conn.ExportSubtreeMethodTable(map[string]interface{}{ + "ContextMenu": v.ContextMenu, + "Activate": v.Activate, + "SecondaryActivate": v.SecondaryActivate, + "Scroll": v.Scroll, + }, path, InterfaceStatusNotifierItem) +} + +// UnexportStatusNotifierItem unexports org.kde.StatusNotifierItem interface on the named path. +func UnexportStatusNotifierItem(conn *dbus.Conn, path dbus.ObjectPath) error { + return conn.Export(nil, path, InterfaceStatusNotifierItem) +} + +// UnimplementedStatusNotifierItem can be embedded to have forward compatible server implementations. +type UnimplementedStatusNotifierItem struct{} + +func (*UnimplementedStatusNotifierItem) iface() string { + return InterfaceStatusNotifierItem +} + +func (*UnimplementedStatusNotifierItem) ContextMenu(x int32, y int32) (err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*UnimplementedStatusNotifierItem) Activate(x int32, y int32) (err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*UnimplementedStatusNotifierItem) SecondaryActivate(x int32, y int32) (err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*UnimplementedStatusNotifierItem) Scroll(delta int32, orientation string) (err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +// NewStatusNotifierItem creates and allocates org.kde.StatusNotifierItem. +func NewStatusNotifierItem(object dbus.BusObject) *StatusNotifierItem { + return &StatusNotifierItem{object} +} + +// StatusNotifierItem implements org.kde.StatusNotifierItem D-Bus interface. +type StatusNotifierItem struct { + object dbus.BusObject +} + +// ContextMenu calls org.kde.StatusNotifierItem.ContextMenu method. +func (o *StatusNotifierItem) ContextMenu(ctx context.Context, x int32, y int32) (err error) { + err = o.object.CallWithContext(ctx, InterfaceStatusNotifierItem+".ContextMenu", 0, x, y).Store() + return +} + +// Activate calls org.kde.StatusNotifierItem.Activate method. +func (o *StatusNotifierItem) Activate(ctx context.Context, x int32, y int32) (err error) { + err = o.object.CallWithContext(ctx, InterfaceStatusNotifierItem+".Activate", 0, x, y).Store() + return +} + +// SecondaryActivate calls org.kde.StatusNotifierItem.SecondaryActivate method. +func (o *StatusNotifierItem) SecondaryActivate(ctx context.Context, x int32, y int32) (err error) { + err = o.object.CallWithContext(ctx, InterfaceStatusNotifierItem+".SecondaryActivate", 0, x, y).Store() + return +} + +// Scroll calls org.kde.StatusNotifierItem.Scroll method. +func (o *StatusNotifierItem) Scroll(ctx context.Context, delta int32, orientation string) (err error) { + err = o.object.CallWithContext(ctx, InterfaceStatusNotifierItem+".Scroll", 0, delta, orientation).Store() + return +} + +// GetCategory gets org.kde.StatusNotifierItem.Category property. +func (o *StatusNotifierItem) GetCategory(ctx context.Context) (category string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "Category").Store(&category) + return +} + +// GetId gets org.kde.StatusNotifierItem.Id property. +func (o *StatusNotifierItem) GetId(ctx context.Context) (id string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "Id").Store(&id) + return +} + +// GetTitle gets org.kde.StatusNotifierItem.Title property. +func (o *StatusNotifierItem) GetTitle(ctx context.Context) (title string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "Title").Store(&title) + return +} + +// GetStatus gets org.kde.StatusNotifierItem.Status property. +func (o *StatusNotifierItem) GetStatus(ctx context.Context) (status string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "Status").Store(&status) + return +} + +// GetWindowId gets org.kde.StatusNotifierItem.WindowId property. +func (o *StatusNotifierItem) GetWindowId(ctx context.Context) (windowId int32, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "WindowId").Store(&windowId) + return +} + +// GetIconThemePath gets org.kde.StatusNotifierItem.IconThemePath property. +func (o *StatusNotifierItem) GetIconThemePath(ctx context.Context) (iconThemePath string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "IconThemePath").Store(&iconThemePath) + return +} + +// GetMenu gets org.kde.StatusNotifierItem.Menu property. +func (o *StatusNotifierItem) GetMenu(ctx context.Context) (menu dbus.ObjectPath, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "Menu").Store(&menu) + return +} + +// GetItemIsMenu gets org.kde.StatusNotifierItem.ItemIsMenu property. +func (o *StatusNotifierItem) GetItemIsMenu(ctx context.Context) (itemIsMenu bool, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "ItemIsMenu").Store(&itemIsMenu) + return +} + +// GetIconName gets org.kde.StatusNotifierItem.IconName property. +func (o *StatusNotifierItem) GetIconName(ctx context.Context) (iconName string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "IconName").Store(&iconName) + return +} + +// GetIconPixmap gets org.kde.StatusNotifierItem.IconPixmap property. +// +// Annotations: +// @org.qtproject.QtDBus.QtTypeName = KDbusImageVector +func (o *StatusNotifierItem) GetIconPixmap(ctx context.Context) (iconPixmap []struct { + V0 int32 + V1 int32 + V2 []byte +}, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "IconPixmap").Store(&iconPixmap) + return +} + +// GetOverlayIconName gets org.kde.StatusNotifierItem.OverlayIconName property. +func (o *StatusNotifierItem) GetOverlayIconName(ctx context.Context) (overlayIconName string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "OverlayIconName").Store(&overlayIconName) + return +} + +// GetOverlayIconPixmap gets org.kde.StatusNotifierItem.OverlayIconPixmap property. +// +// Annotations: +// @org.qtproject.QtDBus.QtTypeName = KDbusImageVector +func (o *StatusNotifierItem) GetOverlayIconPixmap(ctx context.Context) (overlayIconPixmap []struct { + V0 int32 + V1 int32 + V2 []byte +}, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "OverlayIconPixmap").Store(&overlayIconPixmap) + return +} + +// GetAttentionIconName gets org.kde.StatusNotifierItem.AttentionIconName property. +func (o *StatusNotifierItem) GetAttentionIconName(ctx context.Context) (attentionIconName string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "AttentionIconName").Store(&attentionIconName) + return +} + +// GetAttentionIconPixmap gets org.kde.StatusNotifierItem.AttentionIconPixmap property. +// +// Annotations: +// @org.qtproject.QtDBus.QtTypeName = KDbusImageVector +func (o *StatusNotifierItem) GetAttentionIconPixmap(ctx context.Context) (attentionIconPixmap []struct { + V0 int32 + V1 int32 + V2 []byte +}, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "AttentionIconPixmap").Store(&attentionIconPixmap) + return +} + +// GetAttentionMovieName gets org.kde.StatusNotifierItem.AttentionMovieName property. +func (o *StatusNotifierItem) GetAttentionMovieName(ctx context.Context) (attentionMovieName string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "AttentionMovieName").Store(&attentionMovieName) + return +} + +// GetToolTip gets org.kde.StatusNotifierItem.ToolTip property. +// +// Annotations: +// @org.qtproject.QtDBus.QtTypeName = KDbusToolTipStruct +func (o *StatusNotifierItem) GetToolTip(ctx context.Context) (toolTip struct { + V0 string + V1 []struct { + V0 int32 + V1 int32 + V2 []byte + } + V2 string + V3 string +}, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "ToolTip").Store(&toolTip) + return +} + +// StatusNotifierItem_NewTitleSignal represents org.kde.StatusNotifierItem.NewTitle signal. +type StatusNotifierItem_NewTitleSignal struct { + sender string + Path dbus.ObjectPath + Body *StatusNotifierItem_NewTitleSignalBody +} + +// Name returns the signal's name. +func (s *StatusNotifierItem_NewTitleSignal) Name() string { + return "NewTitle" +} + +// Interface returns the signal's interface. +func (s *StatusNotifierItem_NewTitleSignal) Interface() string { + return InterfaceStatusNotifierItem +} + +// Sender returns the signal's sender unique name. +func (s *StatusNotifierItem_NewTitleSignal) Sender() string { + return s.sender +} + +func (s *StatusNotifierItem_NewTitleSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *StatusNotifierItem_NewTitleSignal) values() []interface{} { + return []interface{}{} +} + +// StatusNotifierItem_NewTitleSignalBody is body container. +type StatusNotifierItem_NewTitleSignalBody struct { +} + +// StatusNotifierItem_NewIconSignal represents org.kde.StatusNotifierItem.NewIcon signal. +type StatusNotifierItem_NewIconSignal struct { + sender string + Path dbus.ObjectPath + Body *StatusNotifierItem_NewIconSignalBody +} + +// Name returns the signal's name. +func (s *StatusNotifierItem_NewIconSignal) Name() string { + return "NewIcon" +} + +// Interface returns the signal's interface. +func (s *StatusNotifierItem_NewIconSignal) Interface() string { + return InterfaceStatusNotifierItem +} + +// Sender returns the signal's sender unique name. +func (s *StatusNotifierItem_NewIconSignal) Sender() string { + return s.sender +} + +func (s *StatusNotifierItem_NewIconSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *StatusNotifierItem_NewIconSignal) values() []interface{} { + return []interface{}{} +} + +// StatusNotifierItem_NewIconSignalBody is body container. +type StatusNotifierItem_NewIconSignalBody struct { +} + +// StatusNotifierItem_NewAttentionIconSignal represents org.kde.StatusNotifierItem.NewAttentionIcon signal. +type StatusNotifierItem_NewAttentionIconSignal struct { + sender string + Path dbus.ObjectPath + Body *StatusNotifierItem_NewAttentionIconSignalBody +} + +// Name returns the signal's name. +func (s *StatusNotifierItem_NewAttentionIconSignal) Name() string { + return "NewAttentionIcon" +} + +// Interface returns the signal's interface. +func (s *StatusNotifierItem_NewAttentionIconSignal) Interface() string { + return InterfaceStatusNotifierItem +} + +// Sender returns the signal's sender unique name. +func (s *StatusNotifierItem_NewAttentionIconSignal) Sender() string { + return s.sender +} + +func (s *StatusNotifierItem_NewAttentionIconSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *StatusNotifierItem_NewAttentionIconSignal) values() []interface{} { + return []interface{}{} +} + +// StatusNotifierItem_NewAttentionIconSignalBody is body container. +type StatusNotifierItem_NewAttentionIconSignalBody struct { +} + +// StatusNotifierItem_NewOverlayIconSignal represents org.kde.StatusNotifierItem.NewOverlayIcon signal. +type StatusNotifierItem_NewOverlayIconSignal struct { + sender string + Path dbus.ObjectPath + Body *StatusNotifierItem_NewOverlayIconSignalBody +} + +// Name returns the signal's name. +func (s *StatusNotifierItem_NewOverlayIconSignal) Name() string { + return "NewOverlayIcon" +} + +// Interface returns the signal's interface. +func (s *StatusNotifierItem_NewOverlayIconSignal) Interface() string { + return InterfaceStatusNotifierItem +} + +// Sender returns the signal's sender unique name. +func (s *StatusNotifierItem_NewOverlayIconSignal) Sender() string { + return s.sender +} + +func (s *StatusNotifierItem_NewOverlayIconSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *StatusNotifierItem_NewOverlayIconSignal) values() []interface{} { + return []interface{}{} +} + +// StatusNotifierItem_NewOverlayIconSignalBody is body container. +type StatusNotifierItem_NewOverlayIconSignalBody struct { +} + +// StatusNotifierItem_NewStatusSignal represents org.kde.StatusNotifierItem.NewStatus signal. +type StatusNotifierItem_NewStatusSignal struct { + sender string + Path dbus.ObjectPath + Body *StatusNotifierItem_NewStatusSignalBody +} + +// Name returns the signal's name. +func (s *StatusNotifierItem_NewStatusSignal) Name() string { + return "NewStatus" +} + +// Interface returns the signal's interface. +func (s *StatusNotifierItem_NewStatusSignal) Interface() string { + return InterfaceStatusNotifierItem +} + +// Sender returns the signal's sender unique name. +func (s *StatusNotifierItem_NewStatusSignal) Sender() string { + return s.sender +} + +func (s *StatusNotifierItem_NewStatusSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *StatusNotifierItem_NewStatusSignal) values() []interface{} { + return []interface{}{s.Body.Status} +} + +// StatusNotifierItem_NewStatusSignalBody is body container. +type StatusNotifierItem_NewStatusSignalBody struct { + Status string +} + +// StatusNotifierItem_NewIconThemePathSignal represents org.kde.StatusNotifierItem.NewIconThemePath signal. +type StatusNotifierItem_NewIconThemePathSignal struct { + sender string + Path dbus.ObjectPath + Body *StatusNotifierItem_NewIconThemePathSignalBody +} + +// Name returns the signal's name. +func (s *StatusNotifierItem_NewIconThemePathSignal) Name() string { + return "NewIconThemePath" +} + +// Interface returns the signal's interface. +func (s *StatusNotifierItem_NewIconThemePathSignal) Interface() string { + return InterfaceStatusNotifierItem +} + +// Sender returns the signal's sender unique name. +func (s *StatusNotifierItem_NewIconThemePathSignal) Sender() string { + return s.sender +} + +func (s *StatusNotifierItem_NewIconThemePathSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *StatusNotifierItem_NewIconThemePathSignal) values() []interface{} { + return []interface{}{s.Body.IconThemePath} +} + +// StatusNotifierItem_NewIconThemePathSignalBody is body container. +type StatusNotifierItem_NewIconThemePathSignalBody struct { + IconThemePath string +} + +// StatusNotifierItem_NewMenuSignal represents org.kde.StatusNotifierItem.NewMenu signal. +type StatusNotifierItem_NewMenuSignal struct { + sender string + Path dbus.ObjectPath + Body *StatusNotifierItem_NewMenuSignalBody +} + +// Name returns the signal's name. +func (s *StatusNotifierItem_NewMenuSignal) Name() string { + return "NewMenu" +} + +// Interface returns the signal's interface. +func (s *StatusNotifierItem_NewMenuSignal) Interface() string { + return InterfaceStatusNotifierItem +} + +// Sender returns the signal's sender unique name. +func (s *StatusNotifierItem_NewMenuSignal) Sender() string { + return s.sender +} + +func (s *StatusNotifierItem_NewMenuSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *StatusNotifierItem_NewMenuSignal) values() []interface{} { + return []interface{}{} +} + +// StatusNotifierItem_NewMenuSignalBody is body container. +type StatusNotifierItem_NewMenuSignalBody struct { +} diff --git a/src/systray/systray.go b/vendor/fyne.io/systray/systray.go similarity index 70% rename from src/systray/systray.go rename to vendor/fyne.io/systray/systray.go index 490003d..17ac656 100644 --- a/src/systray/systray.go +++ b/vendor/fyne.io/systray/systray.go @@ -1,9 +1,4 @@ -//go:build darwin || windows -// +build darwin windows - -/* -Package systray is a cross-platform Go library to place an icon and menu in the notification area. -*/ +// Package systray is a cross-platform Go library to place an icon and menu in the notification area. package systray import ( @@ -15,15 +10,29 @@ import ( ) var ( - systrayReady func() - systrayExit func() - menuItems = make(map[uint32]*MenuItem) - menuItemsLock sync.RWMutex + systrayReady, systrayExit func() + tappedLeft, tappedRight func() + systrayExitCalled bool + menuItems = make(map[uint32]*MenuItem) + menuItemsLock sync.RWMutex - currentID = uint32(0) - quitOnce sync.Once + initialMenuBuilt sync.WaitGroup + currentID atomic.Uint32 + quitOnce sync.Once + + // TrayOpenedCh receives an entry each time the system tray menu is opened. + TrayOpenedCh = make(chan struct{}) ) +// This helper function allows us to call systrayExit only once, +// without accidentally calling it twice in the same lifetime. +func runSystrayExit() { + if !systrayExitCalled { + systrayExitCalled = true + systrayExit() + } +} + func init() { runtime.LockOSThread() } @@ -61,7 +70,7 @@ func (item *MenuItem) String() string { func newMenuItem(title string, tooltip string, parent *MenuItem) *MenuItem { return &MenuItem{ ClickedCh: make(chan struct{}), - id: atomic.AddUint32(¤tID, 1), + id: currentID.Add(1), title: title, tooltip: tooltip, disabled: false, @@ -73,11 +82,24 @@ func newMenuItem(title string, tooltip string, parent *MenuItem) *MenuItem { // Run initializes GUI and starts the event loop, then invokes the onReady // callback. It blocks until systray.Quit() is called. -func Run(onReady func(), onExit func()) { +func Run(onReady, onExit func()) { + setInternalLoop(true) Register(onReady, onExit) + nativeLoop() } +// RunWithExternalLoop allows the system tray module to operate with other toolkits. +// The returned start and end functions should be called by the toolkit when the application has started and will end. +func RunWithExternalLoop(onReady, onExit func()) (start, end func()) { + Register(onReady, onExit) + + return nativeStart, func() { + nativeEnd() + Quit() + } +} + // Register initializes GUI and registers the callbacks but relies on the // caller to run the event loop somewhere else. It's useful if the program // needs to show other UI elements, for example, webview. @@ -89,9 +111,11 @@ func Register(onReady func(), onExit func()) { } else { // Run onReady on separate goroutine to avoid blocking event loop readyCh := make(chan interface{}) + initialMenuBuilt.Add(1) go func() { <-readyCh onReady() + initialMenuBuilt.Done() }() systrayReady = func() { close(readyCh) @@ -103,14 +127,36 @@ func Register(onReady func(), onExit func()) { onExit = func() {} } systrayExit = onExit + systrayExitCalled = false registerSystray() } +// ResetMenu will remove all menu items +func ResetMenu() { + menuItemsLock.Lock() + id := currentID.Load() + menuItemsLock.Unlock() + for i, item := range menuItems { + if i < id && item.parent == nil { + item.Remove() + } + } + resetMenu() +} + // Quit the systray func Quit() { quitOnce.Do(quit) } +func SetOnTapped(f func()) { + tappedLeft = f +} + +func SetOnSecondaryTapped(f func()) { + tappedRight = f +} + // 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 @@ -121,8 +167,8 @@ func AddMenuItem(title string, tooltip string) *MenuItem { } // AddMenuItemCheckbox adds a menu item with the designated title and tooltip and a checkbox for Linux. +// On other platforms there will be a check indicated next to the item if `checked` is true. // 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 @@ -133,7 +179,12 @@ func AddMenuItemCheckbox(title string, tooltip string, checked bool) *MenuItem { // AddSeparator adds a separator bar to the menu func AddSeparator() { - addSeparator(atomic.AddUint32(¤tID, 1)) + addSeparator(currentID.Add(1), 0) +} + +// AddSeparator adds a separator bar to the submenu +func (item *MenuItem) AddSeparator() { + addSeparator(currentID.Add(1), item.id) } // AddSubMenuItem adds a nested sub-menu item with the designated title and tooltip. @@ -190,6 +241,30 @@ func (item *MenuItem) Hide() { hideMenuItem(item) } +// Remove removes a menu item +func (item *MenuItem) Remove() { + menuItemsLock.RLock() + var childList []*MenuItem + for _, child := range menuItems { + if child.parent == item { + childList = append(childList, child) + } + } + menuItemsLock.RUnlock() + for _, child := range childList { + child.Remove() + } + removeMenuItem(item) + menuItemsLock.Lock() + delete(menuItems, item.id) + select { + case <-item.ClickedCh: + default: + } + close(item.ClickedCh) + menuItemsLock.Unlock() +} + // Show shows a previously hidden menu item func (item *MenuItem) Show() { showMenuItem(item) @@ -225,7 +300,7 @@ func systrayMenuItemSelected(id uint32) { item, ok := menuItems[id] menuItemsLock.RUnlock() if !ok { - log.Printf("No menu item with ID %v", id) + log.Printf("systray error: no menu item with ID %d\n", id) return } select { diff --git a/src/systray/systray.h b/vendor/fyne.io/systray/systray.h similarity index 66% rename from src/systray/systray.h rename to vendor/fyne.io/systray/systray.h index 888c829..96200bd 100644 --- a/src/systray/systray.h +++ b/vendor/fyne.io/systray/systray.h @@ -2,16 +2,25 @@ extern void systray_ready(); extern void systray_on_exit(); +extern void systray_left_click(); +extern void systray_right_click(); extern void systray_menu_item_selected(int menu_id); +extern void systray_menu_will_open(); void registerSystray(void); +void nativeEnd(void); int nativeLoop(void); +void nativeStart(void); 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 setRemovalAllowed(bool allowed); 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 add_separator(int menuId, int parentId); void hide_menu_item(int menuId); +void remove_menu_item(int menuId); void show_menu_item(int menuId); +void reset_menu(); +void show_menu(); void quit(); diff --git a/vendor/fyne.io/systray/systray_darwin.go b/vendor/fyne.io/systray/systray_darwin.go new file mode 100644 index 0000000..612e75a --- /dev/null +++ b/vendor/fyne.io/systray/systray_darwin.go @@ -0,0 +1,213 @@ +//go:build !ios + +package systray + +/* +#cgo darwin CFLAGS: -DDARWIN -x objective-c -fobjc-arc +#cgo darwin LDFLAGS: -framework Cocoa + +#include +#include "systray.h" + +void setInternalLoop(bool); +*/ +import "C" + +import ( + "fmt" + "os" + "unsafe" +) + +// SetTemplateIcon sets the systray icon as a template icon (on Mac), falling back +// to a regular icon on other platforms. +// templateIconBytes and regularIconBytes should be the content of .ico for windows and +// .ico/.jpg/.png for other platforms. +func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { + cstr := (*C.char)(unsafe.Pointer(&templateIconBytes[0])) + C.setIcon(cstr, (C.int)(len(templateIconBytes)), true) +} + +// SetIcon sets the icon of a menu item. Only works on macOS and Windows. +// iconBytes should be the content of .ico/.jpg/.png +func (item *MenuItem) SetIcon(iconBytes []byte) { + cstr := (*C.char)(unsafe.Pointer(&iconBytes[0])) + C.setMenuItemIcon(cstr, (C.int)(len(iconBytes)), C.int(item.id), false) +} + +// SetIconFromFilePath sets the icon of a menu item from a file path. +// iconFilePath should be the path to a .ico for windows and .ico/.jpg/.png for other platforms. +func (item *MenuItem) SetIconFromFilePath(iconFilePath string) error { + iconBytes, err := os.ReadFile(iconFilePath) + if err != nil { + return fmt.Errorf("failed to read icon file: %v", err) + } + item.SetIcon(iconBytes) + return nil +} + +// SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows, it +// falls back to the regular icon bytes and on Linux it does nothing. +// templateIconBytes and regularIconBytes should be the content of .ico for windows and +// .ico/.jpg/.png for other platforms. +func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { + cstr := (*C.char)(unsafe.Pointer(&templateIconBytes[0])) + C.setMenuItemIcon(cstr, (C.int)(len(templateIconBytes)), C.int(item.id), true) +} + +// SetRemovalAllowed sets whether a user can remove the systray icon or not. +// This is only supported on macOS. +func SetRemovalAllowed(allowed bool) { + C.setRemovalAllowed((C.bool)(allowed)) +} + +func registerSystray() { + C.registerSystray() +} + +func nativeLoop() { + C.nativeLoop() +} + +func nativeEnd() { + C.nativeEnd() +} + +func nativeStart() { + C.nativeStart() +} + +func quit() { + C.quit() +} + +func setInternalLoop(internal bool) { + C.setInternalLoop(C.bool(internal)) +} + +// SetIcon sets the systray icon. +// iconBytes should be the content of .ico for windows and .ico/.jpg/.png +// for other platforms. +func SetIcon(iconBytes []byte) { + cstr := (*C.char)(unsafe.Pointer(&iconBytes[0])) + C.setIcon(cstr, (C.int)(len(iconBytes)), false) +} + +// SetIconFromFilePath sets the systray icon from a file path. +// iconFilePath should be the path to a .ico for windows and .ico/.jpg/.png for other platforms. +func SetIconFromFilePath(iconFilePath string) error { + bytes, err := os.ReadFile(iconFilePath) + if err != nil { + return fmt.Errorf("failed to read icon file: %v", err) + } + SetIcon(bytes) + return nil +} + +// SetTitle sets the systray title, only available on Mac and Linux. +func SetTitle(title string) { + C.setTitle(C.CString(title)) +} + +// SetTooltip sets the systray tooltip to display on mouse hover of the tray icon, +// only available on Mac and Windows. +func SetTooltip(tooltip string) { + C.setTooltip(C.CString(tooltip)) +} + +func addOrUpdateMenuItem(item *MenuItem) { + var disabled C.short + if item.disabled { + disabled = 1 + } + var checked C.short + 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 + } + C.add_or_update_menu_item( + C.int(item.id), + C.int(parentID), + C.CString(item.title), + C.CString(item.tooltip), + disabled, + checked, + isCheckable, + ) +} + +func addSeparator(id uint32, parent uint32) { + C.add_separator(C.int(id), C.int(parent)) +} + +func hideMenuItem(item *MenuItem) { + C.hide_menu_item( + C.int(item.id), + ) +} + +func showMenuItem(item *MenuItem) { + C.show_menu_item( + C.int(item.id), + ) +} + +func removeMenuItem(item *MenuItem) { + C.remove_menu_item( + C.int(item.id), + ) +} + +func resetMenu() { + C.reset_menu() +} + +//export systray_left_click +func systray_left_click() { + if fn := tappedLeft; fn != nil { + fn() + return + } + + C.show_menu() +} + +//export systray_right_click +func systray_right_click() { + if fn := tappedRight; fn != nil { + fn() + return + } + + C.show_menu() +} + +//export systray_ready +func systray_ready() { + systrayReady() +} + +//export systray_on_exit +func systray_on_exit() { + runSystrayExit() +} + +//export systray_menu_item_selected +func systray_menu_item_selected(cID C.int) { + systrayMenuItemSelected(uint32(cID)) +} + +//export systray_menu_will_open +func systray_menu_will_open() { + select { + case TrayOpenedCh <- struct{}{}: + default: + } +} diff --git a/src/systray/systray_darwin.m b/vendor/fyne.io/systray/systray_darwin.m similarity index 55% rename from src/systray/systray_darwin.m rename to vendor/fyne.io/systray/systray_darwin.m index 884fa43..b0e9ee8 100644 --- a/src/systray/systray_darwin.m +++ b/vendor/fyne.io/systray/systray_darwin.m @@ -1,3 +1,5 @@ +//go:build !ios + #import #include "systray.h" @@ -50,13 +52,33 @@ withParentMenuId: (int)theParentMenuId } @end -@interface AppDelegate: NSObject +@interface RightClickDetector : NSView + +@property (copy) void (^onRightClicked)(NSEvent *); + +@end + +@implementation RightClickDetector + +- (void)rightMouseUp:(NSEvent *)theEvent { + if (!self.onRightClicked) { + return; + } + + self.onRightClicked(theEvent); +} + +@end + + +@interface SystrayAppDelegate: NSObject - (void) add_or_update_menu_item:(MenuItem*) item; - (IBAction)menuHandler:(id)sender; + - (void)menuWillOpen:(NSMenu*)menu; @property (assign) IBOutlet NSWindow *window; - @end +@end - @implementation AppDelegate +@implementation SystrayAppDelegate { NSStatusItem *statusItem; NSMenu *menu; @@ -68,17 +90,73 @@ withParentMenuId: (int)theParentMenuId - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { self->statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength]; + self->menu = [[NSMenu alloc] init]; - [self->menu setAutoenablesItems: FALSE]; - [self->statusItem setMenu:self->menu]; + self->menu.delegate = self; + self->menu.autoenablesItems = FALSE; + // Once the user has removed it, the item needs to be explicitly brought back, + // even restarting the application is insufficient. + // Since the interface from Go is relatively simple, for now we ensure it's + // always visible at application startup. + self->statusItem.visible = TRUE; + + NSStatusBarButton *button = self->statusItem.button; + button.action = @selector(leftMouseClicked); + + [NSEvent addLocalMonitorForEventsMatchingMask: (NSEventTypeLeftMouseDown|NSEventTypeRightMouseDown) + handler: ^NSEvent *(NSEvent *event) { + if (event.window != self->statusItem.button.window) { + return event; + } + + [self leftMouseClicked]; + + return nil; + }]; + + NSSize size = [button frame].size; + NSRect frame = CGRectMake(0, 0, size.width, size.height); + RightClickDetector *rightClicker = [[RightClickDetector alloc] initWithFrame:frame]; + rightClicker.onRightClicked = ^(NSEvent *event) { + [self rightMouseClicked]; + }; + + rightClicker.autoresizingMask = (NSViewWidthSizable | + NSViewHeightSizable); + button.autoresizesSubviews = YES; + [button addSubview:rightClicker]; + systray_ready(); } +- (void)rightMouseClicked { + systray_right_click(); +} + +- (void)leftMouseClicked { + systray_left_click(); +} + - (void)applicationWillTerminate:(NSNotification *)aNotification { systray_on_exit(); } +- (void)setRemovalAllowed { + NSStatusItemBehavior behavior = [self->statusItem behavior]; + behavior |= NSStatusItemBehaviorRemovalAllowed; + self->statusItem.behavior = behavior; +} + +- (void)setRemovalForbidden { + NSStatusItemBehavior behavior = [self->statusItem behavior]; + behavior &= ~NSStatusItemBehaviorRemovalAllowed; + // Ensure the menu item is visible if it was removed, since we're now + // disallowing removal. + self->statusItem.visible = TRUE; + self->statusItem.behavior = behavior; +} + - (void)setIcon:(NSImage *)image { statusItem.button.image = image; [self updateTitleButtonStyle]; @@ -89,7 +167,7 @@ withParentMenuId: (int)theParentMenuId [self updateTitleButtonStyle]; } --(void)updateTitleButtonStyle { +- (void)updateTitleButtonStyle { if (statusItem.button.image != nil) { if ([statusItem.button.title length] == 0) { statusItem.button.imagePosition = NSImageOnly; @@ -112,6 +190,10 @@ withParentMenuId: (int)theParentMenuId systray_menu_item_selected(menuId.intValue); } +- (void)menuWillOpen:(NSMenu *)menu { + systray_menu_will_open(); +} + - (void)add_or_update_menu_item:(MenuItem *)item { NSMenu *theMenu = self->menu; NSMenuItem *parentItem; @@ -125,9 +207,8 @@ withParentMenuId: (int)theParentMenuId [parentItem setSubmenu:theMenu]; } } - - NSMenuItem *menuItem; - menuItem = find_menu_item(theMenu, item->menuId); + + NSMenuItem *menuItem = find_menu_item(theMenu, item->menuId); if (menuItem == NULL) { menuItem = [theMenu addItemWithTitle:item->title action:@selector(menuHandler:) @@ -170,8 +251,15 @@ NSMenuItem *find_menu_item(NSMenu *ourMenu, NSNumber *menuId) { return NULL; }; -- (void) add_separator:(NSNumber*) menuId +- (void) add_separator:(NSNumber*) parentMenuId { + if (parentMenuId.integerValue != 0) { + NSMenuItem* menuItem = find_menu_item(menu, parentMenuId); + if (menuItem != NULL) { + [menuItem.submenu addItem: [NSMenuItem separatorItem]]; + return; + } + } [menu addItem: [NSMenuItem separatorItem]]; } @@ -195,6 +283,13 @@ NSMenuItem *find_menu_item(NSMenu *ourMenu, NSNumber *menuId) { menuItem.image = image; } +- (void)show_menu +{ + [self->menu popUpMenuPositioningItem:nil + atLocation:NSMakePoint(0, self->statusItem.button.bounds.size.height+6) + inView:self->statusItem.button]; +} + - (void) show_menu_item:(NSNumber*) menuId { NSMenuItem* menuItem = find_menu_item(menu, menuId); @@ -203,16 +298,55 @@ NSMenuItem *find_menu_item(NSMenu *ourMenu, NSNumber *menuId) { } } +- (void) remove_menu_item:(NSNumber*) menuId +{ + NSMenuItem* menuItem = find_menu_item(menu, menuId); + if (menuItem != NULL) { + [menuItem.menu removeItem:menuItem]; + } +} + +- (void) reset_menu +{ + [self->menu removeAllItems]; +} + - (void) quit { - [NSApp terminate:self]; + // This tells the app event loop to stop after processing remaining messages. + [NSApp stop:self]; + // The event loop won't return until it processes another event. + // https://stackoverflow.com/a/48064752/149482 + NSPoint eventLocation = NSMakePoint(0, 0); + NSEvent *customEvent = [NSEvent otherEventWithType:NSEventTypeApplicationDefined + location:eventLocation + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + subtype:0 + data1:0 + data2:0]; + [NSApp postEvent:customEvent atStart:NO]; } @end +bool internalLoop = false; +SystrayAppDelegate *owner; + +void setInternalLoop(bool i) { + internalLoop = i; +} + void registerSystray(void) { - AppDelegate *delegate = [[AppDelegate alloc] init]; - [[NSApplication sharedApplication] setDelegate:delegate]; + if (!internalLoop) { // with an external loop we don't take ownership of the app + return; + } + + owner = [[SystrayAppDelegate alloc] init]; + [[NSApplication sharedApplication] setDelegate:owner]; + // A workaround to avoid crashing on macOS versions before Catalina. Somehow // SIGSEGV would happen inside AppKit if [NSApp run] is called from a // different function, even if that function is called right after this. @@ -221,6 +355,10 @@ void registerSystray(void) { } } +void nativeEnd(void) { + systray_on_exit(); +} + int nativeLoop(void) { if (floor(NSAppKitVersionNumber) > /*NSAppKitVersionNumber10_14*/ 1671){ [NSApp run]; @@ -228,8 +366,16 @@ int nativeLoop(void) { return EXIT_SUCCESS; } +void nativeStart(void) { + owner = [[SystrayAppDelegate alloc] init]; + + NSNotification *launched = [NSNotification notificationWithName:NSApplicationDidFinishLaunchingNotification + object:[NSApplication sharedApplication]]; + [owner applicationDidFinishLaunching:launched]; +} + void runInMainThread(SEL method, id object) { - [(AppDelegate*)[NSApp delegate] + [owner performSelectorOnMainThread:method withObject:object waitUntilDone: YES]; @@ -237,19 +383,23 @@ void runInMainThread(SEL method, id object) { void setIcon(const char* iconBytes, int length, bool template) { NSData* buffer = [NSData dataWithBytes: iconBytes length:length]; - NSImage *image = [[NSImage alloc] initWithData:buffer]; - [image setSize:NSMakeSize(16, 16)]; - image.template = template; - runInMainThread(@selector(setIcon:), (id)image); + @autoreleasepool { + NSImage *image = [[NSImage alloc] initWithData:buffer]; + [image setSize:NSMakeSize(16, 16)]; + image.template = template; + runInMainThread(@selector(setIcon:), (id)image); + } } void setMenuItemIcon(const char* iconBytes, int length, int menuId, bool template) { NSData* buffer = [NSData dataWithBytes: iconBytes length:length]; - NSImage *image = [[NSImage alloc] initWithData:buffer]; - [image setSize:NSMakeSize(16, 16)]; - image.template = template; - NSNumber *mId = [NSNumber numberWithInt:menuId]; - runInMainThread(@selector(setMenuItemIcon:), @[image, (id)mId]); + @autoreleasepool { + NSImage *image = [[NSImage alloc] initWithData:buffer]; + [image setSize:NSMakeSize(16, 16)]; + image.template = template; + NSNumber *mId = [NSNumber numberWithInt:menuId]; + runInMainThread(@selector(setMenuItemIcon:), @[image, (id)mId]); + } } void setTitle(char* ctitle) { @@ -266,6 +416,14 @@ void setTooltip(char* ctooltip) { runInMainThread(@selector(setTooltip:), (id)tooltip); } +void setRemovalAllowed(bool allowed) { + if (allowed) { + runInMainThread(@selector(setRemovalAllowed), nil); + } else { + runInMainThread(@selector(setRemovalForbidden), nil); + } +} + 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); @@ -273,9 +431,9 @@ void add_or_update_menu_item(int menuId, int parentMenuId, char* title, char* to runInMainThread(@selector(add_or_update_menu_item:), (id)item); } -void add_separator(int menuId) { - NSNumber *mId = [NSNumber numberWithInt:menuId]; - runInMainThread(@selector(add_separator:), (id)mId); +void add_separator(int menuId, int parentId) { + NSNumber *pId = [NSNumber numberWithInt:parentId]; + runInMainThread(@selector(add_separator:), (id)pId); } void hide_menu_item(int menuId) { @@ -283,11 +441,24 @@ void hide_menu_item(int menuId) { runInMainThread(@selector(hide_menu_item:), (id)mId); } +void remove_menu_item(int menuId) { + NSNumber *mId = [NSNumber numberWithInt:menuId]; + runInMainThread(@selector(remove_menu_item:), (id)mId); +} + +void show_menu() { + runInMainThread(@selector(show_menu), nil); +} + void show_menu_item(int menuId) { NSNumber *mId = [NSNumber numberWithInt:menuId]; runInMainThread(@selector(show_menu_item:), (id)mId); } +void reset_menu() { + runInMainThread(@selector(reset_menu), nil); +} + void quit() { runInMainThread(@selector(quit), nil); } diff --git a/vendor/fyne.io/systray/systray_menu_unix.go b/vendor/fyne.io/systray/systray_menu_unix.go new file mode 100644 index 0000000..ae7d242 --- /dev/null +++ b/vendor/fyne.io/systray/systray_menu_unix.go @@ -0,0 +1,370 @@ +//go:build (linux || freebsd || openbsd || netbsd) && !android + +package systray + +import ( + "fmt" + "log" + "os" + + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/prop" + + "fyne.io/systray/internal/generated/menu" +) + +// SetIcon sets the icon of a menu item. +// iconBytes should be the content of .ico/.jpg/.png +func (item *MenuItem) SetIcon(iconBytes []byte) { + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + m, exists := findLayout(int32(item.id)) + if exists { + m.V1["icon-data"] = dbus.MakeVariant(iconBytes) + refresh() + } +} + +// SetIconFromFilePath sets the icon of a menu item from a file path. +// iconFilePath should be the path to a .ico for windows and .ico/.jpg/.png for other platforms. +func (item *MenuItem) SetIconFromFilePath(iconFilePath string) error { + iconBytes, err := os.ReadFile(iconFilePath) + if err != nil { + return fmt.Errorf("failed to read icon file: %v", err) + } + item.SetIcon(iconBytes) + return nil +} + +// copyLayout makes full copy of layout +func copyLayout(in *menuLayout, depth int32) *menuLayout { + out := menuLayout{ + V0: in.V0, + V1: make(map[string]dbus.Variant, len(in.V1)), + } + for k, v := range in.V1 { + out.V1[k] = v + } + if depth != 0 { + depth-- + out.V2 = make([]dbus.Variant, len(in.V2)) + for i, v := range in.V2 { + out.V2[i] = dbus.MakeVariant(copyLayout(v.Value().(*menuLayout), depth)) + } + } else { + out.V2 = []dbus.Variant{} + } + return &out +} + +// GetLayout is com.canonical.dbusmenu.GetLayout method. +func (t *tray) GetLayout(parentID int32, recursionDepth int32, propertyNames []string) (revision uint32, layout menuLayout, err *dbus.Error) { + initialMenuBuilt.Wait() + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + if m, ok := findLayout(parentID); ok { + // return copy of menu layout to prevent panic from cuncurrent access to layout + return instance.menuVersion, *copyLayout(m, recursionDepth), nil + } + return +} + +// GetGroupProperties is com.canonical.dbusmenu.GetGroupProperties method. +func (t *tray) GetGroupProperties(ids []int32, propertyNames []string) (properties []struct { + V0 int32 + V1 map[string]dbus.Variant +}, err *dbus.Error) { + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + for _, id := range ids { + if m, ok := findLayout(id); ok { + p := struct { + V0 int32 + V1 map[string]dbus.Variant + }{ + V0: m.V0, + V1: make(map[string]dbus.Variant, len(m.V1)), + } + for k, v := range m.V1 { + p.V1[k] = v + } + properties = append(properties, p) + } + } + return +} + +// GetProperty is com.canonical.dbusmenu.GetProperty method. +func (t *tray) GetProperty(id int32, name string) (value dbus.Variant, err *dbus.Error) { + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + if m, ok := findLayout(id); ok { + if p, ok := m.V1[name]; ok { + return p, nil + } + } + return +} + +// Event is com.canonical.dbusmenu.Event method. +func (t *tray) Event(id int32, eventID string, data dbus.Variant, timestamp uint32) (err *dbus.Error) { + switch eventID { + case "clicked": + systrayMenuItemSelected(uint32(id)) + case "opened": + t.menuLock.RLock() + rootMenuID := t.menu.V0 + t.menuLock.RUnlock() + + if id == rootMenuID { + select { + case TrayOpenedCh <- struct{}{}: + default: + } + } + } + return +} + +// EventGroup is com.canonical.dbusmenu.EventGroup method. +func (t *tray) EventGroup(events []struct { + V0 int32 + V1 string + V2 dbus.Variant + V3 uint32 +}) (idErrors []int32, err *dbus.Error) { + for _, event := range events { + if event.V1 == "clicked" { + systrayMenuItemSelected(uint32(event.V0)) + } + } + return +} + +// AboutToShow is com.canonical.dbusmenu.AboutToShow method. +func (t *tray) AboutToShow(id int32) (needUpdate bool, err *dbus.Error) { + return +} + +// AboutToShowGroup is com.canonical.dbusmenu.AboutToShowGroup method. +func (t *tray) AboutToShowGroup(ids []int32) (updatesNeeded []int32, idErrors []int32, err *dbus.Error) { + return +} + +func createMenuPropSpec() map[string]map[string]*prop.Prop { + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + return map[string]map[string]*prop.Prop{ + "com.canonical.dbusmenu": { + "Version": { + Value: instance.menuVersion, + Writable: true, + Emit: prop.EmitTrue, + Callback: nil, + }, + "TextDirection": { + Value: "ltr", + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "Status": { + Value: "normal", + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "IconThemePath": { + Value: []string{}, + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + }, + } +} + +// menuLayout is a named struct to map into generated bindings. It represents the layout of a menu item +type menuLayout = struct { + V0 int32 // the unique ID of this item + V1 map[string]dbus.Variant // properties for this menu item layout + V2 []dbus.Variant // child menu item layouts +} + +func addOrUpdateMenuItem(item *MenuItem) { + var layout *menuLayout + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + m, exists := findLayout(int32(item.id)) + if exists { + layout = m + } else { + layout = &menuLayout{ + V0: int32(item.id), + V1: map[string]dbus.Variant{}, + V2: []dbus.Variant{}, + } + + parent := instance.menu + if item.parent != nil { + m, ok := findLayout(int32(item.parent.id)) + if ok { + parent = m + parent.V1["children-display"] = dbus.MakeVariant("submenu") + } + } + parent.V2 = append(parent.V2, dbus.MakeVariant(layout)) + } + + applyItemToLayout(item, layout) + if exists { + refresh() + } +} + +func addSeparator(id uint32, parent uint32) { + menu, _ := findLayout(int32(parent)) + + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + layout := &menuLayout{ + V0: int32(id), + V1: map[string]dbus.Variant{ + "type": dbus.MakeVariant("separator"), + }, + V2: []dbus.Variant{}, + } + menu.V2 = append(menu.V2, dbus.MakeVariant(layout)) + refresh() +} + +func applyItemToLayout(in *MenuItem, out *menuLayout) { + out.V1["enabled"] = dbus.MakeVariant(!in.disabled) + out.V1["label"] = dbus.MakeVariant(in.title) + + if in.isCheckable { + out.V1["toggle-type"] = dbus.MakeVariant("checkmark") + if in.checked { + out.V1["toggle-state"] = dbus.MakeVariant(1) + } else { + out.V1["toggle-state"] = dbus.MakeVariant(0) + } + } else { + out.V1["toggle-type"] = dbus.MakeVariant("") + out.V1["toggle-state"] = dbus.MakeVariant(0) + } +} + +func findLayout(id int32) (*menuLayout, bool) { + if id == 0 { + return instance.menu, true + } + return findSubLayout(id, instance.menu.V2) +} + +func findSubLayout(id int32, vals []dbus.Variant) (*menuLayout, bool) { + for _, i := range vals { + item := i.Value().(*menuLayout) + if item.V0 == id { + return item, true + } + + if len(item.V2) > 0 { + child, ok := findSubLayout(id, item.V2) + if ok { + return child, true + } + } + } + + return nil, false +} + +func removeSubLayout(id int32, vals []dbus.Variant) ([]dbus.Variant, bool) { + for idx, i := range vals { + item := i.Value().(*menuLayout) + if item.V0 == id { + return append(vals[:idx], vals[idx+1:]...), true + } + + if len(item.V2) > 0 { + if child, removed := removeSubLayout(id, item.V2); removed { + return child, true + } + } + } + + return vals, false +} + +func removeMenuItem(item *MenuItem) { + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + + parent := instance.menu + if item.parent != nil { + m, ok := findLayout(int32(item.parent.id)) + if !ok { + return + } + parent = m + } + + if items, removed := removeSubLayout(int32(item.id), parent.V2); removed { + parent.V2 = items + refresh() + } +} + +func hideMenuItem(item *MenuItem) { + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + m, exists := findLayout(int32(item.id)) + if exists { + m.V1["visible"] = dbus.MakeVariant(false) + refresh() + } +} + +func showMenuItem(item *MenuItem) { + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + m, exists := findLayout(int32(item.id)) + if exists { + m.V1["visible"] = dbus.MakeVariant(true) + refresh() + } +} + +func refresh() { + instance.lock.Lock() + defer instance.lock.Unlock() + if instance.conn == nil || instance.menuProps == nil { + return + } + instance.menuVersion++ + dbusErr := instance.menuProps.Set("com.canonical.dbusmenu", "Version", + dbus.MakeVariant(instance.menuVersion)) + if dbusErr != nil { + log.Printf("systray error: failed to update menu version: %v\n", dbusErr) + return + } + err := menu.Emit(instance.conn, &menu.Dbusmenu_LayoutUpdatedSignal{ + Path: menuPath, + Body: &menu.Dbusmenu_LayoutUpdatedSignalBody{ + Revision: instance.menuVersion, + }, + }) + if err != nil { + log.Printf("systray error: failed to emit layout updated signal: %v\n", err) + } + +} + +func resetMenu() { + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + instance.menu = &menuLayout{} + instance.menuVersion++ + refresh() +} diff --git a/vendor/fyne.io/systray/systray_notifier_unix.go b/vendor/fyne.io/systray/systray_notifier_unix.go new file mode 100644 index 0000000..eb84493 --- /dev/null +++ b/vendor/fyne.io/systray/systray_notifier_unix.go @@ -0,0 +1,44 @@ +package systray + +import ( + "fyne.io/systray/internal/generated/notifier" + "github.com/godbus/dbus/v5" +) + +type leftRightNotifierItem struct { +} + +func newLeftRightNotifierItem() notifier.StatusNotifierItemer { + return &leftRightNotifierItem{} +} + +func (i *leftRightNotifierItem) Activate(_, _ int32) *dbus.Error { + if f := tappedLeft; f == nil { + return &dbus.ErrMsgUnknownMethod + } + + tappedLeft() + return nil +} + +func (i *leftRightNotifierItem) ContextMenu(_, _ int32) *dbus.Error { + if f := tappedRight; f == nil { + return &dbus.ErrMsgUnknownMethod + } + + tappedRight() + return nil +} + +func (i *leftRightNotifierItem) SecondaryActivate(_, _ int32) *dbus.Error { + if f := tappedRight; f == nil { + return &dbus.ErrMsgUnknownMethod + } + + tappedRight() + return nil +} + +func (i *leftRightNotifierItem) Scroll(_ int32, _ string) *dbus.Error { + return &dbus.ErrMsgUnknownMethod +} diff --git a/vendor/fyne.io/systray/systray_unix.go b/vendor/fyne.io/systray/systray_unix.go new file mode 100644 index 0000000..bde7d9c --- /dev/null +++ b/vendor/fyne.io/systray/systray_unix.go @@ -0,0 +1,435 @@ +//go:build (linux || freebsd || openbsd || netbsd) && !android + +//Note that you need to have github.com/knightpp/dbus-codegen-go installed from "custom" branch +//go:generate dbus-codegen-go -prefix org.kde -package notifier -output internal/generated/notifier/status_notifier_item.go internal/StatusNotifierItem.xml +//go:generate dbus-codegen-go -prefix com.canonical -package menu -output internal/generated/menu/dbus_menu.go internal/DbusMenu.xml + +package systray + +import ( + "bytes" + "fmt" + "image" + _ "image/png" // used only here + "log" + "os" + "sync" + + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" + "github.com/godbus/dbus/v5/prop" + + "fyne.io/systray/internal/generated/menu" + "fyne.io/systray/internal/generated/notifier" +) + +const ( + path = "/StatusNotifierItem" + menuPath = "/StatusNotifierItem/menu" +) + +var ( + // to signal quitting the internal main loop + quitChan = make(chan struct{}) + + // instance is the current instance of our DBus tray server + instance = &tray{menu: &menuLayout{}, menuVersion: 1} +) + +// SetTemplateIcon sets the systray icon as a template icon (on macOS), falling back +// to a regular icon on other platforms. +// templateIconBytes and iconBytes should be the content of .ico for windows and +// .ico/.jpg/.png for other platforms. +func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { + // TODO handle the templateIconBytes? + SetIcon(regularIconBytes) +} + +// SetIcon sets the systray icon. +// iconBytes should be the content of .ico for windows and .ico/.jpg/.png +// for other platforms. +func SetIcon(iconBytes []byte) { + instance.lock.Lock() + instance.iconData = iconBytes + props := instance.props + conn := instance.conn + defer instance.lock.Unlock() + + if props == nil { + return + } + + props.SetMust("org.kde.StatusNotifierItem", "IconPixmap", + []PX{convertToPixels(iconBytes)}) + if conn == nil { + return + } + + err := notifier.Emit(conn, ¬ifier.StatusNotifierItem_NewIconSignal{ + Path: path, + Body: ¬ifier.StatusNotifierItem_NewIconSignalBody{}, + }) + if err != nil { + log.Printf("systray error: failed to emit new icon signal: %s\n", err) + return + } +} + +// SetIconFromFilePath sets the systray icon from a file path. +// iconFilePath should be the path to a .ico for windows and .ico/.jpg/.png for other platforms. +func SetIconFromFilePath(iconFilePath string) error { + bytes, err := os.ReadFile(iconFilePath) + if err != nil { + return fmt.Errorf("failed to read icon file: %v", err) + } + SetIcon(bytes) + return nil +} + +// SetTitle sets the systray title, only available on Mac and Linux. +func SetTitle(t string) { + instance.lock.Lock() + instance.title = t + props := instance.props + conn := instance.conn + defer instance.lock.Unlock() + + if props == nil { + return + } + dbusErr := props.Set("org.kde.StatusNotifierItem", "Title", + dbus.MakeVariant(t)) + if dbusErr != nil { + log.Printf("systray error: failed to set Title prop: %s\n", dbusErr) + return + } + + if conn == nil { + return + } + + err := notifier.Emit(conn, ¬ifier.StatusNotifierItem_NewTitleSignal{ + Path: path, + Body: ¬ifier.StatusNotifierItem_NewTitleSignalBody{}, + }) + if err != nil { + log.Printf("systray error: failed to emit new title signal: %s\n", err) + return + } +} + +// SetTooltip sets the systray tooltip to display on mouse hover of the tray icon, +// only available on Mac and Windows. +func SetTooltip(tooltipTitle string) { + instance.lock.Lock() + instance.tooltipTitle = tooltipTitle + props := instance.props + defer instance.lock.Unlock() + + if props == nil { + return + } + dbusErr := props.Set("org.kde.StatusNotifierItem", "ToolTip", + dbus.MakeVariant(tooltip{V2: tooltipTitle})) + if dbusErr != nil { + log.Printf("systray error: failed to set ToolTip prop: %s\n", dbusErr) + return + } +} + +// SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows and +// Linux, it falls back to the regular icon bytes. +// templateIconBytes and regularIconBytes should be the content of .ico for windows and +// .ico/.jpg/.png for other platforms. +func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { + item.SetIcon(regularIconBytes) +} + +// SetRemovalAllowed sets whether a user can remove the systray icon or not. +// This is only supported on macOS. +func SetRemovalAllowed(allowed bool) { +} + +func setInternalLoop(_ bool) { + // nothing to action on Linux +} + +func registerSystray() { +} + +func nativeLoop() int { + nativeStart() + <-quitChan + nativeEnd() + return 0 +} + +func nativeEnd() { + runSystrayExit() + instance.conn.Close() +} + +func quit() { + close(quitChan) +} + +func nativeStart() { + systrayReady() + conn, err := dbus.SessionBus() + if err != nil { + log.Printf("systray error: failed to connect to DBus: %v\n", err) + return + } + err = notifier.ExportStatusNotifierItem(conn, path, newLeftRightNotifierItem()) + if err != nil { + log.Printf("systray error: failed to export status notifier item: %v\n", err) + } + err = menu.ExportDbusmenu(conn, menuPath, instance) + if err != nil { + log.Printf("systray error: failed to export status notifier menu: %v\n", err) + return + } + + name := fmt.Sprintf("org.kde.StatusNotifierItem-%d-1", os.Getpid()) // register id 1 for this process + _, err = conn.RequestName(name, dbus.NameFlagDoNotQueue) + if err != nil { + log.Printf("systray error: failed to request name: %s\n", err) + // it's not critical error: continue + } + props, err := prop.Export(conn, path, instance.createPropSpec()) + if err != nil { + log.Printf("systray error: failed to export notifier item properties to bus: %s\n", err) + return + } + menuProps, err := prop.Export(conn, menuPath, createMenuPropSpec()) + if err != nil { + log.Printf("systray error: failed to export notifier menu properties to bus: %s\n", err) + return + } + + node := introspect.Node{ + Name: path, + Interfaces: []introspect.Interface{ + introspect.IntrospectData, + prop.IntrospectData, + notifier.IntrospectDataStatusNotifierItem, + }, + } + err = conn.Export(introspect.NewIntrospectable(&node), path, + "org.freedesktop.DBus.Introspectable") + if err != nil { + log.Printf("systray error: failed to export node introspection: %s\n", err) + return + } + menuNode := introspect.Node{ + Name: menuPath, + Interfaces: []introspect.Interface{ + introspect.IntrospectData, + prop.IntrospectData, + menu.IntrospectDataDbusmenu, + }, + } + err = conn.Export(introspect.NewIntrospectable(&menuNode), menuPath, + "org.freedesktop.DBus.Introspectable") + if err != nil { + log.Printf("systray error: failed to export menu node introspection: %s\n", err) + return + } + + instance.lock.Lock() + instance.conn = conn + instance.props = props + instance.menuProps = menuProps + instance.lock.Unlock() + + go stayRegistered() +} + +func register() bool { + obj := instance.conn.Object("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher") + call := obj.Call("org.kde.StatusNotifierWatcher.RegisterStatusNotifierItem", 0, path) + if call.Err != nil { + log.Printf("systray error: failed to register: %v\n", call.Err) + return false + } + + return true +} + +func stayRegistered() { + register() + + conn := instance.conn + if err := conn.AddMatchSignal( + dbus.WithMatchObjectPath("/org/freedesktop/DBus"), + dbus.WithMatchInterface("org.freedesktop.DBus"), + dbus.WithMatchSender("org.freedesktop.DBus"), + dbus.WithMatchMember("NameOwnerChanged"), + dbus.WithMatchArg(0, "org.kde.StatusNotifierWatcher"), + ); err != nil { + log.Printf("systray error: failed to register signal matching: %v\n", err) + // If we can't monitor signals, there is no point in + // us being here. we're either registered or not (per + // above) and will roll the dice from here... + return + } + + sc := make(chan *dbus.Signal, 10) + conn.Signal(sc) + + for { + select { + case sig := <-sc: + if sig == nil { + return // We get a nil signal when closing the window. + } else if len(sig.Body) < 3 { + return // malformed signal? + } + + // sig.Body has the args, which are [name old_owner new_owner] + if s, ok := sig.Body[2].(string); ok && s != "" { + register() + } + case <-quitChan: + return + } + } +} + +// tray is a basic type that handles the dbus functionality +type tray struct { + // the DBus connection that we will use + conn *dbus.Conn + + // icon data for the main systray icon + iconData []byte + // title and tooltip state + title, tooltipTitle string + + lock sync.Mutex + menu *menuLayout + menuLock sync.RWMutex + props, menuProps *prop.Properties + menuVersion uint32 +} + +func (t *tray) createPropSpec() map[string]map[string]*prop.Prop { + t.lock.Lock() + defer t.lock.Unlock() + id := t.title + if id == "" { + id = fmt.Sprintf("systray_%d", os.Getpid()) + } + return map[string]map[string]*prop.Prop{ + "org.kde.StatusNotifierItem": { + "Status": { + Value: "Active", // Passive, Active or NeedsAttention + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "Title": { + Value: t.title, + Writable: true, + Emit: prop.EmitTrue, + Callback: nil, + }, + "Id": { + Value: id, + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "Category": { + Value: "ApplicationStatus", + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "IconName": { + Value: "", + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "IconPixmap": { + Value: []PX{convertToPixels(t.iconData)}, + Writable: true, + Emit: prop.EmitTrue, + Callback: nil, + }, + "IconThemePath": { + Value: "", + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "ItemIsMenu": { + Value: tappedLeft == nil && tappedRight == nil, + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "Menu": { + Value: dbus.ObjectPath(menuPath), + Writable: true, + Emit: prop.EmitTrue, + Callback: nil, + }, + "ToolTip": { + Value: tooltip{V2: t.tooltipTitle}, + Writable: true, + Emit: prop.EmitTrue, + Callback: nil, + }, + }} +} + +// PX is picture pix map structure with width and high +type PX struct { + W, H int + Pix []byte +} + +// tooltip is our data for a tooltip property. +// Param names need to match the generated code... +type tooltip = struct { + V0 string // name + V1 []PX // icons + V2 string // title + V3 string // description +} + +func convertToPixels(data []byte) PX { + if len(data) == 0 { + return PX{} + } + + img, _, err := image.Decode(bytes.NewReader(data)) + if err != nil { + log.Printf("Failed to read icon format %v", err) + return PX{} + } + + return PX{ + img.Bounds().Dx(), img.Bounds().Dy(), + argbForImage(img), + } +} + +func argbForImage(img image.Image) []byte { + w, h := img.Bounds().Dx(), img.Bounds().Dy() + data := make([]byte, w*h*4) + i := 0 + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + r, g, b, a := img.At(x, y).RGBA() + data[i] = byte(a) + data[i+1] = byte(r) + data[i+2] = byte(g) + data[i+3] = byte(b) + i += 4 + } + } + return data +} diff --git a/src/systray/systray_windows.go b/vendor/fyne.io/systray/systray_windows.go similarity index 75% rename from src/systray/systray_windows.go rename to vendor/fyne.io/systray/systray_windows.go index d4c2ee7..84ee6a2 100644 --- a/src/systray/systray_windows.go +++ b/vendor/fyne.io/systray/systray_windows.go @@ -1,17 +1,19 @@ //go:build windows -// +build windows package systray import ( "crypto/md5" "encoding/hex" + "errors" + "fmt" "io/ioutil" "log" "os" "path/filepath" "sort" "sync" + "sync/atomic" "syscall" "unsafe" @@ -24,6 +26,7 @@ var ( g32 = windows.NewLazySystemDLL("Gdi32.dll") pCreateCompatibleBitmap = g32.NewProc("CreateCompatibleBitmap") pCreateCompatibleDC = g32.NewProc("CreateCompatibleDC") + pCreateDIBSection = g32.NewProc("CreateDIBSection") pDeleteDC = g32.NewProc("DeleteDC") pSelectObject = g32.NewProc("SelectObject") @@ -39,12 +42,13 @@ var ( pCreateWindowEx = u32.NewProc("CreateWindowExW") pDefWindowProc = u32.NewProc("DefWindowProcW") pDeleteMenu = u32.NewProc("DeleteMenu") + pDestroyMenu = u32.NewProc("DestroyMenu") + pRemoveMenu = u32.NewProc("RemoveMenu") pDestroyWindow = u32.NewProc("DestroyWindow") pDispatchMessage = u32.NewProc("DispatchMessageW") pDrawIconEx = u32.NewProc("DrawIconEx") pGetCursorPos = u32.NewProc("GetCursorPos") pGetDC = u32.NewProc("GetDC") - pGetMenuItemID = u32.NewProc("GetMenuItemID") pGetMessage = u32.NewProc("GetMessageW") pGetSystemMetrics = u32.NewProc("GetSystemMetrics") pInsertMenuItem = u32.NewProc("InsertMenuItemW") @@ -64,6 +68,9 @@ var ( pTranslateMessage = u32.NewProc("TranslateMessage") pUnregisterClass = u32.NewProc("UnregisterClassW") pUpdateWindow = u32.NewProc("UpdateWindow") + + // ErrTrayNotReadyYet is returned by functions when they are called before the tray has been initialized. + ErrTrayNotReadyYet = errors.New("tray not ready yet") ) // Contains window class information. @@ -175,6 +182,30 @@ type point struct { X, Y int32 } +// The BITMAPINFO structure defines the dimensions and color information for a DIB. +// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfo +type bitmapInfo struct { + BmiHeader bitmapInfoHeader + BmiColors windows.Handle +} + +// The BITMAPINFOHEADER structure contains information about the dimensions and color format of a device-independent bitmap (DIB). +// https://learn.microsoft.com/en-us/previous-versions/dd183376(v=vs.85) +// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader +type bitmapInfoHeader struct { + BiSize uint32 + BiWidth int32 + BiHeight int32 + BiPlanes uint16 + BiBitCount uint16 + BiCompression uint32 + BiSizeImage uint32 + BiXPelsPerMeter int32 + BiYPelsPerMeter int32 + BiClrUsed uint32 + BiClrImportant uint32 +} + // Contains information about loaded resources type winTray struct { instance, @@ -205,11 +236,22 @@ type winTray struct { wmSystrayMessage, wmTaskbarCreated uint32 + + initialized atomic.Bool +} + +// isReady checks if the tray as already been initialized. It is not goroutine safe with in regard to the initialization function, but prevents a panic when functions are called too early. +func (t *winTray) isReady() bool { + return t.initialized.Load() } // Loads an image from file and shows it in tray. // Shell_NotifyIcon: https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159(v=vs.85).aspx func (t *winTray) setIcon(src string) error { + if !wt.isReady() { + return ErrTrayNotReadyYet + } + const NIF_ICON = 0x00000002 h, err := t.loadIconFrom(src) @@ -229,6 +271,10 @@ func (t *winTray) setIcon(src string) error { // Sets tooltip on icon. // Shell_NotifyIcon: https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159(v=vs.85).aspx func (t *winTray) setTooltip(src string) error { + if !wt.isReady() { + return ErrTrayNotReadyYet + } + const NIF_TIP = 0x00000004 b, err := windows.UTF16FromString(src) if err != nil { @@ -244,15 +290,11 @@ func (t *winTray) setTooltip(src string) error { return t.nid.modify() } -var wt winTray +var wt = winTray{} // WindowProc callback function that processes messages sent to a window. // https://msdn.microsoft.com/en-us/library/windows/desktop/ms633573(v=vs.85).aspx -func (t *winTray) wndProc( - hWnd windows.Handle, - message uint32, - wParam, lParam uintptr, -) (lResult uintptr) { +func (t *winTray) wndProc(hWnd windows.Handle, message uint32, wParam, lParam uintptr) (lResult uintptr) { const ( WM_RBUTTONUP = 0x0205 WM_LBUTTONUP = 0x0202 @@ -260,11 +302,8 @@ func (t *winTray) wndProc( WM_ENDSESSION = 0x0016 WM_CLOSE = 0x0010 WM_DESTROY = 0x0002 - WM_CREATE = 0x0001 ) switch message { - case WM_CREATE: - systrayReady() case WM_COMMAND: menuItemId := int32(wParam) // https://docs.microsoft.com/en-us/windows/win32/menurc/wm-command#menus @@ -284,11 +323,13 @@ func (t *winTray) wndProc( t.nid.delete() } t.muNID.Unlock() - systrayExit() + runSystrayExit() case t.wmSystrayMessage: switch lParam { - case WM_RBUTTONUP, WM_LBUTTONUP: - t.showMenu() + case WM_LBUTTONUP: + systrayLeftClick() + case WM_RBUTTONUP: + systrayRightClick() } case t.wmTaskbarCreated: // on explorer.exe restarts t.muNID.Lock() @@ -498,12 +539,16 @@ func (t *winTray) convertToSubMenu(menuItemId uint32) (windows.Handle, error) { return menu, nil } -func (t *winTray) addOrUpdateMenuItem( - menuItemId uint32, - parentId uint32, - title string, - disabled, checked bool, -) error { +// SetRemovalAllowed sets whether a user can remove the systray icon or not. +// This is only supported on macOS. +func SetRemovalAllowed(allowed bool) { +} + +func (t *winTray) addOrUpdateMenuItem(menuItemId uint32, parentId uint32, title string, disabled, checked bool) error { + if !wt.isReady() { + return ErrTrayNotReadyYet + } + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx const ( MIIM_FTYPE = 0x00000100 @@ -568,6 +613,14 @@ func (t *winTray) addOrUpdateMenuItem( } if res == 0 { + // Menu item does not already exist, create it + t.muMenus.RLock() + submenu, exists := t.menus[menuItemId] + t.muMenus.RUnlock() + if exists { + mi.Mask |= MIIM_SUBMENU + mi.SubMenu = submenu + } t.addToVisibleItems(parentId, menuItemId) position := t.getVisibleItemIndex(parentId, menuItemId) res, _, err = pInsertMenuItem.Call( @@ -589,6 +642,10 @@ func (t *winTray) addOrUpdateMenuItem( } func (t *winTray) addSeparatorMenuItem(menuItemId, parentId uint32) error { + if !wt.isReady() { + return ErrTrayNotReadyYet + } + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx const ( MIIM_FTYPE = 0x00000100 @@ -623,8 +680,11 @@ func (t *winTray) addSeparatorMenuItem(menuItemId, parentId uint32) error { return nil } -func (t *winTray) hideMenuItem(menuItemId, parentId uint32) error { - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647629(v=vs.85).aspx +func (t *winTray) removeMenuItem(menuItemId, parentId uint32) error { + if !wt.isReady() { + return ErrTrayNotReadyYet + } + const MF_BYCOMMAND = 0x00000000 const ERROR_SUCCESS syscall.Errno = 0 @@ -644,7 +704,35 @@ func (t *winTray) hideMenuItem(menuItemId, parentId uint32) error { return nil } +func (t *winTray) hideMenuItem(menuItemId, parentId uint32) error { + if !wt.isReady() { + return ErrTrayNotReadyYet + } + + const MF_BYCOMMAND = 0x00000000 + const ERROR_SUCCESS syscall.Errno = 0 + + t.muMenus.RLock() + menu := uintptr(t.menus[parentId]) + t.muMenus.RUnlock() + res, _, err := pRemoveMenu.Call( + menu, + uintptr(menuItemId), + MF_BYCOMMAND, + ) + if res == 0 && err.(syscall.Errno) != ERROR_SUCCESS { + return err + } + t.delFromVisibleItems(parentId, menuItemId) + + return nil +} + func (t *winTray) showMenu() error { + if !wt.isReady() { + return ErrTrayNotReadyYet + } + const ( TPM_BOTTOMALIGN = 0x0020 TPM_LEFTALIGN = 0x0000 @@ -678,7 +766,7 @@ func (t *winTray) delFromVisibleItems(parent, val uint32) { visibleItems := t.visibleItems[parent] for i, itemval := range visibleItems { if val == itemval { - visibleItems = append(visibleItems[:i], visibleItems[i+1:]...) + t.visibleItems[parent] = append(visibleItems[:i], visibleItems[i+1:]...) break } } @@ -710,6 +798,10 @@ func (t *winTray) getVisibleItemIndex(parent, val uint32) int { // Loads an image from file to be shown in tray or menu item. // LoadImage: https://msdn.microsoft.com/en-us/library/windows/desktop/ms648045(v=vs.85).aspx func (t *winTray) loadIconFrom(src string) (windows.Handle, error) { + if !wt.isReady() { + return 0, ErrTrayNotReadyYet + } + const IMAGE_ICON = 1 // Loads an icon const LR_LOADFROMFILE = 0x00000010 // Loads the stand-alone image from the file const LR_DEFAULTSIZE = 0x00000040 // Loads default-size icon for windows(SM_CXICON x SM_CYICON) if cx, cy are set to zero @@ -742,7 +834,7 @@ func (t *winTray) loadIconFrom(src string) (windows.Handle, error) { return h, nil } -func (t *winTray) iconToBitmap(hIcon windows.Handle) (windows.Handle, error) { +func iconToBitmap(hIcon windows.Handle) (windows.Handle, error) { const SM_CXSMICON = 49 const SM_CYSMICON = 50 const DI_NORMAL = 0x3 @@ -758,10 +850,7 @@ func (t *winTray) iconToBitmap(hIcon windows.Handle) (windows.Handle, error) { defer pDeleteDC.Call(hMemDC) cx, _, _ := pGetSystemMetrics.Call(SM_CXSMICON) cy, _, _ := pGetSystemMetrics.Call(SM_CYSMICON) - hMemBmp, _, err := pCreateCompatibleBitmap.Call(hDC, cx, cy) - if hMemBmp == 0 { - return 0, err - } + hMemBmp, err := create32BitHBitmap(hMemDC, int32(cx), int32(cy)) hOriginalBmp, _, _ := pSelectObject.Call(hMemDC, hMemBmp) defer pSelectObject.Call(hMemDC, hOriginalBmp) res, _, err := pDrawIconEx.Call(hMemDC, 0, 0, uintptr(hIcon), cx, cy, 0, uintptr(0), DI_NORMAL) @@ -771,49 +860,94 @@ func (t *winTray) iconToBitmap(hIcon windows.Handle) (windows.Handle, error) { return windows.Handle(hMemBmp), nil } +// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createdibsection +func create32BitHBitmap(hDC uintptr, cx, cy int32) (uintptr, error) { + const BI_RGB uint32 = 0 + const DIB_RGB_COLORS = 0 + bmi := bitmapInfo{ + BmiHeader: bitmapInfoHeader{ + BiPlanes: 1, + BiCompression: BI_RGB, + BiWidth: cx, + BiHeight: cy, + BiBitCount: 32, + }, + } + bmi.BmiHeader.BiSize = uint32(unsafe.Sizeof(bmi.BmiHeader)) + var bits uintptr + hBitmap, _, err := pCreateDIBSection.Call( + hDC, + uintptr(unsafe.Pointer(&bmi)), + DIB_RGB_COLORS, + uintptr(unsafe.Pointer(&bits)), + uintptr(0), + 0, + ) + if hBitmap == 0 { + return 0, err + } + return hBitmap, nil +} + func registerSystray() { if err := wt.initInstance(); err != nil { - log.Printf("Unable to init instance: %v", err) + log.Printf("systray error: unable to init instance: %s\n", err) return } if err := wt.createMenu(); err != nil { - log.Printf("Unable to create menu: %v", err) + log.Printf("systray error: unable to create menu: %s\n", err) return } + wt.initialized.Store(true) + systrayReady() } -func nativeLoop() { - // Main message pump. - m := &struct { - WindowHandle windows.Handle - Message uint32 - Wparam uintptr - Lparam uintptr - Time uint32 - Pt point - }{} - for { - ret, _, err := pGetMessage.Call(uintptr(unsafe.Pointer(m)), 0, 0, 0) +var m = &struct { + WindowHandle windows.Handle + Message uint32 + Wparam uintptr + Lparam uintptr + Time uint32 + Pt point +}{} - // If the function retrieves a message other than WM_QUIT, the return value is nonzero. - // If the function retrieves the WM_QUIT message, the return value is zero. - // If there is an error, the return value is -1 - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644936(v=vs.85).aspx - switch int32(ret) { - case -1: - log.Printf("Error at message loop: %v", err) - return - case 0: - return - default: - pTranslateMessage.Call(uintptr(unsafe.Pointer(m))) - pDispatchMessage.Call(uintptr(unsafe.Pointer(m))) - } +func nativeLoop() { + for doNativeTick() { } } +func nativeEnd() { +} + +func nativeStart() { + go func() { + for doNativeTick() { + } + }() +} + +func doNativeTick() bool { + ret, _, err := pGetMessage.Call(uintptr(unsafe.Pointer(m)), 0, 0, 0) + + // If the function retrieves a message other than WM_QUIT, the return value is nonzero. + // If the function retrieves the WM_QUIT message, the return value is zero. + // If there is an error, the return value is -1 + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644936(v=vs.85).aspx + switch int32(ret) { + case -1: + log.Printf("systray error: message loop failure: %s\n", err) + return false + case 0: + return false + default: + pTranslateMessage.Call(uintptr(unsafe.Pointer(m))) + pDispatchMessage.Call(uintptr(unsafe.Pointer(m))) + } + return true +} + func quit() { const WM_CLOSE = 0x0010 @@ -823,6 +957,16 @@ func quit() { 0, 0, ) + + wt.muNID.Lock() + if wt.nid != nil { + wt.nid.delete() + } + wt.muNID.Unlock() + runSystrayExit() +} + +func setInternalLoop(bool) { } func iconBytesToFilePath(iconBytes []byte) (string, error) { @@ -844,15 +988,25 @@ func iconBytesToFilePath(iconBytes []byte) (string, error) { func SetIcon(iconBytes []byte) { iconFilePath, err := iconBytesToFilePath(iconBytes) if err != nil { - log.Printf("Unable to write icon data to temp file: %v", err) + log.Printf("systray error: unable to write icon data to temp file: %s\n", err) return } if err := wt.setIcon(iconFilePath); err != nil { - log.Printf("Unable to set icon: %v", err) + log.Printf("systray error: unable to set icon: %s\n", err) return } } +// SetIconFromFilePath sets the systray icon from a file path. +// iconFilePath should be the path to a .ico for windows and .ico/.jpg/.png for other platforms. +func SetIconFromFilePath(iconFilePath string) error { + err := wt.setIcon(iconFilePath) + if err != nil { + return fmt.Errorf("failed to set icon: %v", err) + } + return nil +} + // SetTemplateIcon sets the systray icon as a template icon (on macOS), falling back // to a regular icon on other platforms. // templateIconBytes and iconBytes should be the content of .ico for windows and @@ -878,57 +1032,53 @@ func (item *MenuItem) parentId() uint32 { func (item *MenuItem) SetIcon(iconBytes []byte) { iconFilePath, err := iconBytesToFilePath(iconBytes) if err != nil { - log.Printf("Unable to write icon data to temp file: %v", err) + log.Printf("systray error: unable to write icon data to temp file: %s\n", err) return } + err = item.SetIconFromFilePath(iconFilePath) + if err != nil { + log.Printf("systray error: %s\n", err) + return + } +} + +// SetIconFromFilePath sets the icon of a menu item from a file path. +// iconFilePath should be the path to a .ico for windows and .ico/.jpg/.png for other platforms. +func (item *MenuItem) SetIconFromFilePath(iconFilePath string) error { h, err := wt.loadIconFrom(iconFilePath) if err != nil { - log.Printf("Unable to load icon from temp file: %v", err) - return + return fmt.Errorf("unable to load icon from file: %s", err) } - h, err = wt.iconToBitmap(h) + h, err = iconToBitmap(h) if err != nil { - log.Printf("Unable to convert icon to bitmap: %v", err) - return + return fmt.Errorf("unable to convert icon to bitmap: %s", err) } wt.muMenuItemIcons.Lock() wt.menuItemIcons[uint32(item.id)] = h wt.muMenuItemIcons.Unlock() - err = wt.addOrUpdateMenuItem( - uint32(item.id), - item.parentId(), - item.title, - item.disabled, - item.checked, - ) + err = wt.addOrUpdateMenuItem(uint32(item.id), item.parentId(), item.title, item.disabled, item.checked) if err != nil { - log.Printf("Unable to addOrUpdateMenuItem: %v", err) - return + return fmt.Errorf("unable to addOrUpdateMenuItem: %s", err) } + return nil } // SetTooltip sets the systray tooltip to display on mouse hover of the tray icon, // only available on Mac and Windows. func SetTooltip(tooltip string) { if err := wt.setTooltip(tooltip); err != nil { - log.Printf("Unable to set tooltip: %v", err) + log.Printf("systray error: unable to set tooltip: %s\n", err) return } } func addOrUpdateMenuItem(item *MenuItem) { - err := wt.addOrUpdateMenuItem( - uint32(item.id), - item.parentId(), - item.title, - item.disabled, - item.checked, - ) + err := wt.addOrUpdateMenuItem(uint32(item.id), item.parentId(), item.title, item.disabled, item.checked) if err != nil { - log.Printf("Unable to addOrUpdateMenuItem: %v", err) + log.Printf("systray error: unable to addOrUpdateMenuItem: %s\n", err) return } } @@ -941,10 +1091,10 @@ func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes item.SetIcon(regularIconBytes) } -func addSeparator(id uint32) { - err := wt.addSeparatorMenuItem(id, 0) +func addSeparator(id uint32, parent uint32) { + err := wt.addSeparatorMenuItem(id, parent) if err != nil { - log.Printf("Unable to addSeparator: %v", err) + log.Printf("systray error: unable to addSeparator: %s\n", err) return } } @@ -952,7 +1102,15 @@ func addSeparator(id uint32) { func hideMenuItem(item *MenuItem) { err := wt.hideMenuItem(uint32(item.id), item.parentId()) if err != nil { - log.Printf("Unable to hideMenuItem: %v", err) + log.Printf("systray error: unable to hideMenuItem: %s\n", err) + return + } +} + +func removeMenuItem(item *MenuItem) { + err := wt.removeMenuItem(uint32(item.id), item.parentId()) + if err != nil { + log.Printf("systray error: unable to removeMenuItem: %s\n", err) return } } @@ -960,3 +1118,30 @@ func hideMenuItem(item *MenuItem) { func showMenuItem(item *MenuItem) { addOrUpdateMenuItem(item) } + +func resetMenu() { + _, _, _ = pDestroyMenu.Call(uintptr(wt.menus[0])) + wt.visibleItems = make(map[uint32][]uint32) + wt.menus = make(map[uint32]windows.Handle) + wt.menuOf = make(map[uint32]windows.Handle) + wt.menuItemIcons = make(map[uint32]windows.Handle) + wt.createMenu() +} + +func systrayLeftClick() { + if fn := tappedLeft; fn != nil { + fn() + return + } + + wt.showMenu() +} + +func systrayRightClick() { + if fn := tappedRight; fn != nil { + fn() + return + } + + wt.showMenu() +} diff --git a/vendor/github.com/godbus/dbus/v5/CONTRIBUTING.md b/vendor/github.com/godbus/dbus/v5/CONTRIBUTING.md new file mode 100644 index 0000000..c88f9b2 --- /dev/null +++ b/vendor/github.com/godbus/dbus/v5/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# How to Contribute + +## Getting Started + +- Fork the repository on GitHub +- Read the [README](README.markdown) for build and test instructions +- Play with the project, submit bugs, submit patches! + +## Contribution Flow + +This is a rough outline of what a contributor's workflow looks like: + +- Create a topic branch from where you want to base your work (usually master). +- Make commits of logical units. +- Make sure your commit messages are in the proper format (see below). +- Push your changes to a topic branch in your fork of the repository. +- Make sure the tests pass, and add any new tests as appropriate. +- Submit a pull request to the original repository. + +Thanks for your contributions! + +### Format of the Commit Message + +We follow a rough convention for commit messages that is designed to answer two +questions: what changed and why. The subject line should feature the what and +the body of the commit should describe the why. + +``` +scripts: add the test-cluster command + +this uses tmux to setup a test cluster that you can easily kill and +start for debugging. + +Fixes #38 +``` + +The format can be described more formally as follows: + +``` +: + + + +