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 b389d99d6..d5b905917 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; @@ -274,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( @@ -355,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) { @@ -601,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) @@ -609,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 @@ -1141,3 +1151,58 @@ func runSaveFileDialog(dialog *SaveFileDialogStruct) (string, error) { return results[0], nil } + +// 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) + C.app_indicator_set_status(indicator, C.APP_INDICATOR_STATUS_ACTIVE) + 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))) +} + +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/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 f2cee68d1..4e76f8986 100644 --- a/v3/pkg/application/systemtray_linux.go +++ b/v3/pkg/application/systemtray_linux.go @@ -1,9 +1,7 @@ -//go:build linux +//go:build linux package application -import "fmt" - type linuxSystemTray struct { id uint label string @@ -12,6 +10,8 @@ type linuxSystemTray struct { iconPosition int isTemplateIcon bool + tray pointer + nativeMenu pointer } func (s *linuxSystemTray) setIconPosition(position int) { @@ -19,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 { @@ -37,49 +39,49 @@ 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) + // s.menu.Update() + s.setMenu(s.menu) } - }) } 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) }) } @@ -93,16 +95,17 @@ func newSystemTrayImpl(s *SystemTray) systemTrayImpl { isTemplateIcon: s.isTemplateIcon, } } - func (s *linuxSystemTray) openMenu() { + if s.tray == nil { + return + } } func (s *linuxSystemTray) setLabel(label string) { s.label = label - // C.systemTraySetLabel(s.nsStatusItem, C.CString(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 {