From fa6adad4ab54299ddda254d250e21f077e2939c5 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 21 Sep 2023 15:46:37 -0500 Subject: [PATCH 1/3] [v3 linux] wip: systray implementation --- v3/pkg/application/linux_cgo.go | 73 +++++++++++++++++++++++++- v3/pkg/application/systemtray_linux.go | 32 +++++------ 2 files changed, 89 insertions(+), 16 deletions(-) diff --git a/v3/pkg/application/linux_cgo.go b/v3/pkg/application/linux_cgo.go index b389d99d6..49443ad5a 100644 --- a/v3/pkg/application/linux_cgo.go +++ b/v3/pkg/application/linux_cgo.go @@ -4,6 +4,9 @@ package application import ( "fmt" + "log" + "os" + "path/filepath" "strings" "unsafe" @@ -12,7 +15,7 @@ import ( ) /* -#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 +#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 ayatana-appindicator3-0.1 #include #include @@ -20,12 +23,15 @@ import ( #include #include #include + #ifdef G_APPLICATION_DEFAULT_FLAGS #define APPLICATION_DEFAULT_FLAGS G_APPLICATION_DEFAULT_FLAGS #else #define APPLICATION_DEFAULT_FLAGS G_APPLICATION_FLAGS_NONE #endif +#include + typedef struct CallbackID { unsigned int value; @@ -1141,3 +1147,68 @@ func runSaveFileDialog(dialog *SaveFileDialogStruct) (string, error) { return results[0], nil } + +// systray +func systrayNew(label string) pointer { + labelStr := C.CString(label) + defer C.free(unsafe.Pointer(labelStr)) + emptyStr := C.CString("") + defer C.free(unsafe.Pointer(emptyStr)) + indicator := C.app_indicator_new(labelStr, labelStr, C.APP_INDICATOR_CATEGORY_APPLICATION_STATUS) + + trayMenu := C.gtk_menu_new() + // item := C.gtk_menu_item_new_with_label(labelStr) + // C.gtk_menu_shell_append((*C.GtkMenuShell)(unsafe.Pointer(trayMenu)), item) + // C.gtk_widget_show(item) + C.app_indicator_set_status(indicator, C.APP_INDICATOR_STATUS_ACTIVE) + C.app_indicator_set_menu(indicator, (*C.GtkMenu)(unsafe.Pointer(trayMenu))) + iconStr := C.CString("wails-systray-icon") + defer C.free(unsafe.Pointer(iconStr)) + C.app_indicator_set_icon_full(indicator, iconStr, emptyStr) + systraySetLabel(pointer(indicator), label) + return pointer(indicator) +} + +func systraySetTitle(tray pointer, title string) { + titleStr := C.CString(title) + defer C.free(unsafe.Pointer(titleStr)) + C.app_indicator_set_title((*C.AppIndicator)(tray), titleStr) +} + +func systraySetLabel(tray pointer, label string) { + labelStr := C.CString(label) + defer C.free(unsafe.Pointer(labelStr)) + emptyStr := C.CString("") + defer C.free(unsafe.Pointer(emptyStr)) + C.app_indicator_set_label((*C.AppIndicator)(tray), labelStr, labelStr) +} + +func systrayMenuSet(tray pointer, menu pointer) { + // C.app_indicator_set_menu((*C.AppIndicator)(tray), (*C.GtkMenu)(unsafe.Pointer(menu))) +} + +/* +GdkPixbuf *pixbuf = gdk_pixbuf_new_from_file(icon_file_path, NULL); +int width, height; +gdk_pixbuf_get_file_info (icon_file_path, &width, &height); +gtk_icon_theme_add_builtin_icon ("custom_icon", width, pixbuf); +g_object_unref (G_OBJECT (pixbuf)); + +GtkToolItem *toolbar_item = gtk_toggle_tool_button_new(); +gtk_tool_button_set_icon_name (GTK_TOOL_BUTTON(toolbar_item), "custom_icon"); +*/ + +func systraySetTemplateIcon(tray pointer, icon []byte) { + dirname, err := os.UserHomeDir() + if err != nil { + log.Fatal(err) + } + file := filepath.Join(dirname, ".icons", "wails-systray-icon.png") + if err := os.WriteFile(file, icon, 0666); err != nil { + log.Fatal(err) + } + systrayStr := C.CString("wails-systray-icon") + defer C.free(unsafe.Pointer(systrayStr)) + + C.app_indicator_set_attention_icon_full((*C.AppIndicator)(tray), systrayStr, systrayStr) +} diff --git a/v3/pkg/application/systemtray_linux.go b/v3/pkg/application/systemtray_linux.go index f2cee68d1..7503d451c 100644 --- a/v3/pkg/application/systemtray_linux.go +++ b/v3/pkg/application/systemtray_linux.go @@ -1,4 +1,4 @@ -//go:build linux +//go:build linux package application @@ -12,6 +12,7 @@ type linuxSystemTray struct { iconPosition int isTemplateIcon bool + tray pointer } func (s *linuxSystemTray) setIconPosition(position int) { @@ -37,49 +38,50 @@ func (s *linuxSystemTray) bounds() (*Rect, error) { func (s *linuxSystemTray) run() { InvokeSync(func() { + label := s.label + if label == "" { + label = "Wails" + } + s.tray = systrayNew(label) + // if s.nsStatusItem != nil { // Fatal("System tray '%d' already running", s.id) // } // s.nsStatusItem = unsafe.Pointer(C.systemTrayNew()) if s.label != "" { - // C.systemTraySetLabel(s.nsStatusItem, C.CString(s.label)) + systraySetTitle(s.tray, s.label) } if s.icon != nil { - // s.nsImage = unsafe.Pointer(C.imageFromBytes((*C.uchar)(&s.icon[0]), C.int(len(s.icon)))) - // C.systemTraySetIcon(s.nsStatusItem, s.nsImage, C.int(s.iconPosition), C.bool(s.isTemplateIcon)) + s.setIcon(s.icon) } if s.menu != nil { s.menu.Update() - // Convert impl to macosMenu object - // s.nsMenu = (s.menu.impl).(*macosMenu).nsMenu - // C.systemTraySetMenu(s.nsStatusItem, s.nsMenu) + menu := (s.menu.impl).(*linuxMenu).menu + systrayMenuSet(s.tray, (menu.impl).(*linuxMenu).native) } - }) } func (s *linuxSystemTray) setIcon(icon []byte) { s.icon = icon InvokeSync(func() { - // s.nsImage = unsafe.Pointer(C.imageFromBytes((*C.uchar)(&icon[0]), C.int(len(icon)))) - // C.systemTraySetIcon(s.nsStatusItem, s.nsImage, C.int(s.iconPosition), C.bool(s.isTemplateIcon)) + systraySetTemplateIcon(s.tray, icon) }) } func (s *linuxSystemTray) setDarkModeIcon(icon []byte) { s.icon = icon InvokeSync(func() { - // s.nsImage = unsafe.Pointer(C.imageFromBytes((*C.uchar)(&icon[0]), C.int(len(icon)))) - // C.systemTraySetIcon(s.nsStatusItem, s.nsImage, C.int(s.iconPosition), C.bool(s.isTemplateIcon)) + systraySetTemplateIcon(s.tray, icon) }) } func (s *linuxSystemTray) setTemplateIcon(icon []byte) { s.icon = icon s.isTemplateIcon = true + globalApplication.dispatchOnMainThread(func() { - // s.nsImage = unsafe.Pointer(C.imageFromBytes((*C.uchar)(&icon[0]), C.int(len(icon)))) - // C.systemTraySetIcon(s.nsStatusItem, s.nsImage, C.int(s.iconPosition), C.bool(s.isTemplateIcon)) + systraySetTemplateIcon(s.tray, icon) }) } @@ -99,7 +101,7 @@ func (s *linuxSystemTray) openMenu() { func (s *linuxSystemTray) setLabel(label string) { s.label = label - // C.systemTraySetLabel(s.nsStatusItem, C.CString(label)) + systraySetLabel(s.tray, label) } func (s *linuxSystemTray) destroy() { From 71fc222059abde1f31cbb5f25406491ba0ba48b3 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 21 Sep 2023 15:56:51 -0500 Subject: [PATCH 2/3] [v3 linux] systray: cleanup + add basic menu --- v3/pkg/application/linux_cgo.go | 17 +++-------------- v3/pkg/application/systemtray_linux.go | 5 +++++ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/v3/pkg/application/linux_cgo.go b/v3/pkg/application/linux_cgo.go index 49443ad5a..7413afff8 100644 --- a/v3/pkg/application/linux_cgo.go +++ b/v3/pkg/application/linux_cgo.go @@ -1157,9 +1157,9 @@ func systrayNew(label string) pointer { indicator := C.app_indicator_new(labelStr, labelStr, C.APP_INDICATOR_CATEGORY_APPLICATION_STATUS) trayMenu := C.gtk_menu_new() - // item := C.gtk_menu_item_new_with_label(labelStr) - // C.gtk_menu_shell_append((*C.GtkMenuShell)(unsafe.Pointer(trayMenu)), item) - // C.gtk_widget_show(item) + item := C.gtk_menu_item_new_with_label(labelStr) + C.gtk_menu_shell_append((*C.GtkMenuShell)(unsafe.Pointer(trayMenu)), item) + C.gtk_widget_show(item) C.app_indicator_set_status(indicator, C.APP_INDICATOR_STATUS_ACTIVE) C.app_indicator_set_menu(indicator, (*C.GtkMenu)(unsafe.Pointer(trayMenu))) iconStr := C.CString("wails-systray-icon") @@ -1187,17 +1187,6 @@ func systrayMenuSet(tray pointer, menu pointer) { // C.app_indicator_set_menu((*C.AppIndicator)(tray), (*C.GtkMenu)(unsafe.Pointer(menu))) } -/* -GdkPixbuf *pixbuf = gdk_pixbuf_new_from_file(icon_file_path, NULL); -int width, height; -gdk_pixbuf_get_file_info (icon_file_path, &width, &height); -gtk_icon_theme_add_builtin_icon ("custom_icon", width, pixbuf); -g_object_unref (G_OBJECT (pixbuf)); - -GtkToolItem *toolbar_item = gtk_toggle_tool_button_new(); -gtk_tool_button_set_icon_name (GTK_TOOL_BUTTON(toolbar_item), "custom_icon"); -*/ - func systraySetTemplateIcon(tray pointer, icon []byte) { dirname, err := os.UserHomeDir() if err != nil { diff --git a/v3/pkg/application/systemtray_linux.go b/v3/pkg/application/systemtray_linux.go index 7503d451c..1a97b6504 100644 --- a/v3/pkg/application/systemtray_linux.go +++ b/v3/pkg/application/systemtray_linux.go @@ -95,6 +95,11 @@ func newSystemTrayImpl(s *SystemTray) systemTrayImpl { isTemplateIcon: s.isTemplateIcon, } } +func (s *linuxSystemTray) openMenu() { + if s.tray == nil { + return + } +} func (s *linuxSystemTray) openMenu() { } From efa67cb01c5d489d7f2346f86cc54de3a4c38014 Mon Sep 17 00:00:00 2001 From: Travis McLane Date: Thu, 28 Sep 2023 14:50:38 -0500 Subject: [PATCH 3/3] [v3 linux] systray implementation Linux requires a `gtk_menu_bar` for a gtk_window to display a menu. For the `systray` a `gtk_menu` is needed instead. This change creates the correct type of `impl` for the `Menu` depending on how it is being used. --- v3/pkg/application/application_linux.go | 1 + v3/pkg/application/linux_cgo.go | 35 ++++++++++++---------- v3/pkg/application/menu_linux.go | 8 +++++ v3/pkg/application/menuitem_linux.go | 1 + v3/pkg/application/systemtray_linux.go | 18 +++++------ v3/pkg/application/webview_window_linux.go | 5 ++-- 6 files changed, 39 insertions(+), 29 deletions(-) diff --git a/v3/pkg/application/application_linux.go b/v3/pkg/application/application_linux.go index 17dc6a7cd..27c04a89f 100644 --- a/v3/pkg/application/application_linux.go +++ b/v3/pkg/application/application_linux.go @@ -82,6 +82,7 @@ func (m *linuxApp) getApplicationMenu() pointer { menu := globalApplication.ApplicationMenu if menu != nil { + menu.impl = newMenuBarImpl(menu) InvokeSync(func() { menu.Update() }) diff --git a/v3/pkg/application/linux_cgo.go b/v3/pkg/application/linux_cgo.go index 7413afff8..d5b905917 100644 --- a/v3/pkg/application/linux_cgo.go +++ b/v3/pkg/application/linux_cgo.go @@ -280,10 +280,11 @@ func menuAddSeparator(menu *Menu) { C.gtk_separator_menu_item_new()) } -func menuAppend(parent *Menu, menu *MenuItem) { +func menuAppend(parent *Menu, item *MenuItem) { + C.gtk_menu_shell_append( (*C.GtkMenuShell)((parent.impl).(*linuxMenu).native), - (*C.GtkWidget)((menu.impl).(*linuxMenuItem).native), + (*C.GtkWidget)((item.impl).(*linuxMenuItem).native), ) /* gtk4 C.gtk_menu_item_set_submenu( @@ -361,13 +362,17 @@ func menuItemChecked(widget pointer) bool { func menuItemNew(label string) pointer { cLabel := C.CString(label) defer C.free(unsafe.Pointer(cLabel)) - return pointer(C.gtk_menu_item_new_with_label(cLabel)) + item := C.gtk_menu_item_new_with_label(cLabel) + C.gtk_widget_show((*C.GtkWidget)(item)) + return pointer(item) } func menuCheckItemNew(label string) pointer { cLabel := C.CString(label) defer C.free(unsafe.Pointer(cLabel)) - return pointer(C.gtk_check_menu_item_new_with_label(cLabel)) + item := C.gtk_check_menu_item_new_with_label(cLabel) + C.gtk_widget_show((*C.GtkWidget)(item)) + return pointer(item) } func menuItemSetChecked(widget pointer, checked bool) { @@ -607,7 +612,7 @@ func windowMinimize(window pointer) { C.gtk_window_iconify((*C.GtkWindow)(window)) } -func windowNew(application pointer, menu pointer, windowId uint, gpuPolicy int) (window, webview, vbox pointer) { +func windowNew(application pointer, menuBar pointer, windowId uint, gpuPolicy int) (window, webview, vbox pointer) { window = pointer(C.gtk_application_window_new((*C.GtkApplication)(application))) C.g_object_ref_sink(C.gpointer(window)) webview = windowNewWebview(windowId, gpuPolicy) @@ -615,10 +620,9 @@ func windowNew(application pointer, menu pointer, windowId uint, gpuPolicy int) name := C.CString("webview-box") defer C.free(unsafe.Pointer(name)) C.gtk_widget_set_name((*C.GtkWidget)(vbox), name) - C.gtk_container_add((*C.GtkContainer)(window), (*C.GtkWidget)(vbox)) - if menu != nil { - C.gtk_box_pack_start((*C.GtkBox)(vbox), (*C.GtkWidget)(menu), 0, 0, 0) + if menuBar != nil { + C.gtk_box_pack_start((*C.GtkBox)(vbox), (*C.GtkWidget)(menuBar), 0, 0, 0) } C.gtk_box_pack_start((*C.GtkBox)(unsafe.Pointer(vbox)), (*C.GtkWidget)(webview), 1, 1, 0) return @@ -1149,19 +1153,18 @@ func runSaveFileDialog(dialog *SaveFileDialogStruct) (string, error) { } // systray +func systrayDestroy(tray pointer) { + // FIXME: this is not the correct way to remove our systray + // C.g_object_unref(C.gpointer(tray)) +} + func systrayNew(label string) pointer { labelStr := C.CString(label) defer C.free(unsafe.Pointer(labelStr)) emptyStr := C.CString("") defer C.free(unsafe.Pointer(emptyStr)) indicator := C.app_indicator_new(labelStr, labelStr, C.APP_INDICATOR_CATEGORY_APPLICATION_STATUS) - - trayMenu := C.gtk_menu_new() - item := C.gtk_menu_item_new_with_label(labelStr) - C.gtk_menu_shell_append((*C.GtkMenuShell)(unsafe.Pointer(trayMenu)), item) - C.gtk_widget_show(item) C.app_indicator_set_status(indicator, C.APP_INDICATOR_STATUS_ACTIVE) - C.app_indicator_set_menu(indicator, (*C.GtkMenu)(unsafe.Pointer(trayMenu))) iconStr := C.CString("wails-systray-icon") defer C.free(unsafe.Pointer(iconStr)) C.app_indicator_set_icon_full(indicator, iconStr, emptyStr) @@ -1184,7 +1187,9 @@ func systraySetLabel(tray pointer, label string) { } func systrayMenuSet(tray pointer, menu pointer) { - // C.app_indicator_set_menu((*C.AppIndicator)(tray), (*C.GtkMenu)(unsafe.Pointer(menu))) + C.app_indicator_set_menu( + (*C.AppIndicator)(tray), + (*C.GtkMenu)(unsafe.Pointer(menu))) } func systraySetTemplateIcon(tray pointer, icon []byte) { diff --git a/v3/pkg/application/menu_linux.go b/v3/pkg/application/menu_linux.go index 8a5592bdd..19e3bfa12 100644 --- a/v3/pkg/application/menu_linux.go +++ b/v3/pkg/application/menu_linux.go @@ -8,6 +8,14 @@ type linuxMenu struct { } func newMenuImpl(menu *Menu) *linuxMenu { + result := &linuxMenu{ + menu: menu, + native: menuNew(), + } + return result +} + +func newMenuBarImpl(menu *Menu) *linuxMenu { result := &linuxMenu{ menu: menu, native: menuBarNew(), diff --git a/v3/pkg/application/menuitem_linux.go b/v3/pkg/application/menuitem_linux.go index cce9414bb..943104f7c 100644 --- a/v3/pkg/application/menuitem_linux.go +++ b/v3/pkg/application/menuitem_linux.go @@ -103,6 +103,7 @@ func newMenuItemImpl(item *MenuItem) *linuxMenuItem { default: panic(fmt.Sprintf("Unknown menu type: %v", item.itemType)) } + result.setDisabled(result.menuItem.disabled) return result } diff --git a/v3/pkg/application/systemtray_linux.go b/v3/pkg/application/systemtray_linux.go index 1a97b6504..4e76f8986 100644 --- a/v3/pkg/application/systemtray_linux.go +++ b/v3/pkg/application/systemtray_linux.go @@ -2,8 +2,6 @@ package application -import "fmt" - type linuxSystemTray struct { id uint label string @@ -13,6 +11,7 @@ type linuxSystemTray struct { iconPosition int isTemplateIcon bool tray pointer + nativeMenu pointer } func (s *linuxSystemTray) setIconPosition(position int) { @@ -20,8 +19,10 @@ func (s *linuxSystemTray) setIconPosition(position int) { } func (s *linuxSystemTray) setMenu(menu *Menu) { - fmt.Println("linuxSystemTray.SetMenu") s.menu = menu + s.menu.impl = newMenuImpl(menu) + menu.Update() + systrayMenuSet(s.tray, (menu.impl).(*linuxMenu).native) } func (s *linuxSystemTray) positionWindow(window *WebviewWindow, offset int) error { @@ -55,9 +56,8 @@ func (s *linuxSystemTray) run() { s.setIcon(s.icon) } if s.menu != nil { - s.menu.Update() - menu := (s.menu.impl).(*linuxMenu).menu - systrayMenuSet(s.tray, (menu.impl).(*linuxMenu).native) + // s.menu.Update() + s.setMenu(s.menu) } }) } @@ -101,15 +101,11 @@ func (s *linuxSystemTray) openMenu() { } } -func (s *linuxSystemTray) openMenu() { -} - func (s *linuxSystemTray) setLabel(label string) { s.label = label systraySetLabel(s.tray, label) } func (s *linuxSystemTray) destroy() { - // Remove the status item from the status bar and its associated menu - // C.systemTrayDestroy(s.nsStatusItem) + systrayDestroy(s.tray) } diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go index ca3dad5ad..891c66840 100644 --- a/v3/pkg/application/webview_window_linux.go +++ b/v3/pkg/application/webview_window_linux.go @@ -353,9 +353,8 @@ func (w *linuxWebviewWindow) run() { } app := getNativeApplication() - - menu := app.getApplicationMenu() - w.window, w.webview, w.vbox = windowNew(app.application, menu, w.parent.id, 1) + nativeMenu := app.getApplicationMenu() + w.window, w.webview, w.vbox = windowNew(app.application, nativeMenu, w.parent.id, 1) app.registerWindow(w.window, w.parent.id) // record our mapping w.connectSignals() if w.parent.options.EnableDragAndDrop {