From 642fa42f25bc761cae674c80e3cd1853466bd9a8 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sat, 29 Jan 2022 07:19:14 +1100 Subject: [PATCH] Feature/v2 linux menus (#1114) * Render menubar + text menu items * Support disabled menuitems + callbacks * Support checkboxes * Support reusing checkboxes * Support submenus * Support Radio menuitems * Support Menu Accelerators --- v2/internal/frontend/desktop/linux/gtk.go | 73 +++++++++ v2/internal/frontend/desktop/linux/keys.go | 103 +++++++++++++ v2/internal/frontend/desktop/linux/menu.go | 152 ++++++++++++++++++- v2/internal/frontend/desktop/linux/window.go | 38 ++++- 4 files changed, 357 insertions(+), 9 deletions(-) create mode 100644 v2/internal/frontend/desktop/linux/gtk.go create mode 100644 v2/internal/frontend/desktop/linux/keys.go diff --git a/v2/internal/frontend/desktop/linux/gtk.go b/v2/internal/frontend/desktop/linux/gtk.go new file mode 100644 index 000000000..1592ccdc7 --- /dev/null +++ b/v2/internal/frontend/desktop/linux/gtk.go @@ -0,0 +1,73 @@ +package linux + +/* +#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 + +#include "gtk/gtk.h" + +static GtkCheckMenuItem *toGtkCheckMenuItem(void *pointer) { return (GTK_CHECK_MENU_ITEM(pointer)); } + +extern void blockClick(GtkWidget* menuItem, gulong handler_id); +extern void unblockClick(GtkWidget* menuItem, gulong handler_id); +*/ +import "C" +import "unsafe" +import "github.com/wailsapp/wails/v2/pkg/menu" + +func GtkMenuItemWithLabel(label string) *C.GtkWidget { + cLabel := C.CString(label) + result := C.gtk_menu_item_new_with_label(cLabel) + C.free(unsafe.Pointer(cLabel)) + return result +} + +func GtkCheckMenuItemWithLabel(label string) *C.GtkWidget { + cLabel := C.CString(label) + result := C.gtk_check_menu_item_new_with_label(cLabel) + C.free(unsafe.Pointer(cLabel)) + return result +} + +func GtkRadioMenuItemWithLabel(label string, group *C.GSList) *C.GtkWidget { + cLabel := C.CString(label) + result := C.gtk_radio_menu_item_new_with_label(group, cLabel) + C.free(unsafe.Pointer(cLabel)) + return result +} + +//export handleMenuItemClick +func handleMenuItemClick(gtkWidget unsafe.Pointer) { + item := gtkSignalToMenuItem[(*C.GtkWidget)(gtkWidget)] + switch item.Type { + case menu.CheckboxType: + item.Checked = !item.Checked + checked := C.int(0) + if item.Checked { + checked = C.int(1) + } + for _, gtkCheckbox := range gtkCheckboxCache[item] { + handler := gtkSignalHandlers[gtkCheckbox] + C.blockClick(gtkCheckbox, handler) + C.gtk_check_menu_item_set_active(C.toGtkCheckMenuItem(unsafe.Pointer(gtkCheckbox)), checked) + C.unblockClick(gtkCheckbox, handler) + } + item.Click(&menu.CallbackData{MenuItem: item}) + case menu.RadioType: + gtkRadioItems := gtkRadioMenuCache[item] + active := C.gtk_check_menu_item_get_active(C.toGtkCheckMenuItem(gtkWidget)) + if int(active) == 1 { + for _, gtkRadioItem := range gtkRadioItems { + handler := gtkSignalHandlers[gtkRadioItem] + C.blockClick(gtkRadioItem, handler) + C.gtk_check_menu_item_set_active(C.toGtkCheckMenuItem(unsafe.Pointer(gtkRadioItem)), 1) + C.unblockClick(gtkRadioItem, handler) + } + item.Checked = true + item.Click(&menu.CallbackData{MenuItem: item}) + } else { + item.Checked = false + } + default: + item.Click(&menu.CallbackData{MenuItem: item}) + } +} diff --git a/v2/internal/frontend/desktop/linux/keys.go b/v2/internal/frontend/desktop/linux/keys.go new file mode 100644 index 000000000..3d3f9d504 --- /dev/null +++ b/v2/internal/frontend/desktop/linux/keys.go @@ -0,0 +1,103 @@ +package linux + +/* +#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 + +#include "gtk/gtk.h" + +*/ +import "C" +import ( + "github.com/wailsapp/wails/v2/pkg/menu/keys" +) + +var namedKeysToGTK = map[string]C.guint{ + "backspace": C.guint(0xff08), + "tab": C.guint(0xff09), + "return": C.guint(0xff0d), + "enter": C.guint(0xff0d), + "escape": C.guint(0xff1b), + "left": C.guint(0xff51), + "right": C.guint(0xff53), + "up": C.guint(0xff52), + "down": C.guint(0xff54), + "space": C.guint(0xff80), + "delete": C.guint(0xff9f), + "home": C.guint(0xff95), + "end": C.guint(0xff9c), + "page up": C.guint(0xff9a), + "page down": C.guint(0xff9b), + "f1": C.guint(0xffbe), + "f2": C.guint(0xffbf), + "f3": C.guint(0xffc0), + "f4": C.guint(0xffc1), + "f5": C.guint(0xffc2), + "f6": C.guint(0xffc3), + "f7": C.guint(0xffc4), + "f8": C.guint(0xffc5), + "f9": C.guint(0xffc6), + "f10": C.guint(0xffc7), + "f11": C.guint(0xffc8), + "f12": C.guint(0xffc9), + "f13": C.guint(0xffca), + "f14": C.guint(0xffcb), + "f15": C.guint(0xffcc), + "f16": C.guint(0xffcd), + "f17": C.guint(0xffce), + "f18": C.guint(0xffcf), + "f19": C.guint(0xffd0), + "f20": C.guint(0xffd1), + "f21": C.guint(0xffd2), + "f22": C.guint(0xffd3), + "f23": C.guint(0xffd4), + "f24": C.guint(0xffd5), + "f25": C.guint(0xffd6), + "f26": C.guint(0xffd7), + "f27": C.guint(0xffd8), + "f28": C.guint(0xffd9), + "f29": C.guint(0xffda), + "f30": C.guint(0xffdb), + "f31": C.guint(0xffdc), + "f32": C.guint(0xffdd), + "f33": C.guint(0xffde), + "f34": C.guint(0xffdf), + "f35": C.guint(0xffe0), + "numlock": C.guint(0xff7f), +} + +func acceleratorToGTK(accelerator *keys.Accelerator) (C.guint, C.GdkModifierType) { + key := parseKey(accelerator.Key) + mods := parseModifiers(accelerator.Modifiers) + return key, mods +} + +func parseKey(key string) C.guint { + var result C.guint + result, found := namedKeysToGTK[key] + if found { + return result + } + // Check for unknown namedkeys + if len(key) > 1 { + return C.guint(0) + } + keyval := rune(key[0]) + return C.gdk_unicode_to_keyval(C.guint(keyval)) +} + +func parseModifiers(modifiers []keys.Modifier) C.GdkModifierType { + + var result C.GdkModifierType + + for _, modifier := range modifiers { + switch modifier { + case keys.ShiftKey: + result |= C.GDK_SHIFT_MASK + case keys.ControlKey, keys.CmdOrCtrlKey: + result |= C.GDK_CONTROL_MASK + case keys.OptionOrAltKey: + result |= C.GDK_MOD1_MASK + } + } + return result +} diff --git a/v2/internal/frontend/desktop/linux/menu.go b/v2/internal/frontend/desktop/linux/menu.go index 0524429aa..f24e5a5e9 100644 --- a/v2/internal/frontend/desktop/linux/menu.go +++ b/v2/internal/frontend/desktop/linux/menu.go @@ -3,12 +3,160 @@ package linux +/* +#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 + +#include "gtk/gtk.h" + +static GtkMenuItem *toGtkMenuItem(void *pointer) { return (GTK_MENU_ITEM(pointer)); } +static GtkMenuShell *toGtkMenuShell(void *pointer) { return (GTK_MENU_SHELL(pointer)); } +static GtkCheckMenuItem *toGtkCheckMenuItem(void *pointer) { return (GTK_CHECK_MENU_ITEM(pointer)); } +static GtkRadioMenuItem *toGtkRadioMenuItem(void *pointer) { return (GTK_RADIO_MENU_ITEM(pointer)); } + +extern void handleMenuItemClick(void*); + +void blockClick(GtkWidget* menuItem, gulong handler_id) { + g_signal_handler_block (menuItem, handler_id); +} + +void unblockClick(GtkWidget* menuItem, gulong handler_id) { + g_signal_handler_unblock (menuItem, handler_id); +} + +gulong connectClick(GtkWidget* menuItem) { + return g_signal_connect(menuItem, "activate", G_CALLBACK(handleMenuItemClick), (void*)menuItem); +} + +void addAccelerator(GtkWidget* menuItem, GtkAccelGroup* group, guint key, GdkModifierType mods) { + gtk_widget_add_accelerator(menuItem, "activate", group, key, mods, GTK_ACCEL_VISIBLE); +} +*/ +import "C" import "github.com/wailsapp/wails/v2/pkg/menu" +import "unsafe" + +var menuIdCounter int +var menuItemToId map[*menu.MenuItem]int +var menuIdToItem map[int]*menu.MenuItem +var gtkCheckboxCache map[*menu.MenuItem][]*C.GtkWidget +var gtkMenuCache map[*menu.MenuItem]*C.GtkWidget +var gtkRadioMenuCache map[*menu.MenuItem][]*C.GtkWidget +var gtkSignalHandlers map[*C.GtkWidget]C.gulong +var gtkSignalToMenuItem map[*C.GtkWidget]*menu.MenuItem func (f *Frontend) MenuSetApplicationMenu(menu *menu.Menu) { - panic("implement me") + f.mainWindow.SetApplicationMenu(menu) } func (f *Frontend) MenuUpdateApplicationMenu() { - panic("implement me") + //processMenu(f.mainWindow, f.mainWindow.applicationMenu) +} + +func (w *Window) SetApplicationMenu(inmenu *menu.Menu) { + if inmenu == nil { + return + } + + // Setup accelerator group + w.accels = C.gtk_accel_group_new() + C.gtk_window_add_accel_group(w.asGTKWindow(), w.accels) + + menuItemToId = make(map[*menu.MenuItem]int) + menuIdToItem = make(map[int]*menu.MenuItem) + gtkCheckboxCache = make(map[*menu.MenuItem][]*C.GtkWidget) + gtkMenuCache = make(map[*menu.MenuItem]*C.GtkWidget) + gtkRadioMenuCache = make(map[*menu.MenuItem][]*C.GtkWidget) + gtkSignalHandlers = make(map[*C.GtkWidget]C.gulong) + gtkSignalToMenuItem = make(map[*C.GtkWidget]*menu.MenuItem) + + // Increase ref count? + w.menubar = C.gtk_menu_bar_new() + + processMenu(w, inmenu) + + C.gtk_widget_show(w.menubar) +} + +func processMenu(window *Window, menu *menu.Menu) { + for _, menuItem := range menu.Items { + submenu := processSubmenu(menuItem, window.accels) + C.gtk_menu_shell_append(C.toGtkMenuShell(unsafe.Pointer(window.menubar)), submenu) + } +} + +func processSubmenu(menuItem *menu.MenuItem, group *C.GtkAccelGroup) *C.GtkWidget { + existingMenu := gtkMenuCache[menuItem] + if existingMenu != nil { + return existingMenu + } + gtkMenu := C.gtk_menu_new() + submenu := GtkMenuItemWithLabel(menuItem.Label) + for _, menuItem := range menuItem.SubMenu.Items { + menuID := menuIdCounter + menuIdToItem[menuID] = menuItem + menuItemToId[menuItem] = menuID + menuIdCounter++ + processMenuItem(gtkMenu, menuItem, group) + } + C.gtk_menu_item_set_submenu(C.toGtkMenuItem(unsafe.Pointer(submenu)), gtkMenu) + gtkMenuCache[menuItem] = existingMenu + return submenu +} + +var currentRadioGroup *C.GSList + +func processMenuItem(parent *C.GtkWidget, menuItem *menu.MenuItem, group *C.GtkAccelGroup) { + if menuItem.Hidden { + return + } + + if menuItem.Type != menu.RadioType { + currentRadioGroup = nil + } + + if menuItem.Type == menu.SeparatorType { + result := C.gtk_separator_menu_item_new() + C.gtk_menu_shell_append(C.toGtkMenuShell(unsafe.Pointer(parent)), result) + return + } + + var result *C.GtkWidget + + switch menuItem.Type { + case menu.TextType: + result = GtkMenuItemWithLabel(menuItem.Label) + case menu.CheckboxType: + result = GtkCheckMenuItemWithLabel(menuItem.Label) + if menuItem.Checked { + C.gtk_check_menu_item_set_active(C.toGtkCheckMenuItem(unsafe.Pointer(result)), 1) + } + gtkCheckboxCache[menuItem] = append(gtkCheckboxCache[menuItem], result) + + case menu.RadioType: + result = GtkRadioMenuItemWithLabel(menuItem.Label, currentRadioGroup) + currentRadioGroup = C.gtk_radio_menu_item_get_group(C.toGtkRadioMenuItem(unsafe.Pointer(result))) + if menuItem.Checked { + C.gtk_check_menu_item_set_active(C.toGtkCheckMenuItem(unsafe.Pointer(result)), 1) + } + gtkRadioMenuCache[menuItem] = append(gtkRadioMenuCache[menuItem], result) + case menu.SubmenuType: + result = processSubmenu(menuItem, group) + } + C.gtk_menu_shell_append(C.toGtkMenuShell(unsafe.Pointer(parent)), result) + C.gtk_widget_show(result) + + if menuItem.Click != nil { + handler := C.connectClick(result) + gtkSignalHandlers[result] = handler + gtkSignalToMenuItem[result] = menuItem + } + + if menuItem.Disabled { + C.gtk_widget_set_sensitive(result, 0) + } + + if menuItem.Accelerator != nil { + key, mods := acceleratorToGTK(menuItem.Accelerator) + C.addAccelerator(result, group, key, mods) + } } diff --git a/v2/internal/frontend/desktop/linux/window.go b/v2/internal/frontend/desktop/linux/window.go index f27733f75..385ff4988 100644 --- a/v2/internal/frontend/desktop/linux/window.go +++ b/v2/internal/frontend/desktop/linux/window.go @@ -19,6 +19,14 @@ static GtkWindow* GTKWINDOW(void *pointer) { return GTK_WINDOW(pointer); } +static GtkContainer* GTKCONTAINER(void *pointer) { + return GTK_CONTAINER(pointer); +} + +static GtkBox* GTKBOX(void *pointer) { + return GTK_BOX(pointer); +} + static void SetMinSize(GtkWindow* window, int width, int height) { GdkGeometry size; size.min_height = height; @@ -153,7 +161,7 @@ gboolean close_button_pressed(GtkWidget *widget, GdkEvent *event, void* data) GtkWidget* setupWebview(void* contentManager, GtkWindow* window, int hideWindowOnClose) { GtkWidget* webview = webkit_web_view_new_with_user_content_manager((WebKitUserContentManager*)contentManager); - gtk_container_add(GTK_CONTAINER(window), webview); + //gtk_container_add(GTK_CONTAINER(window), webview); WebKitWebContext *context = webkit_web_context_get_default(); webkit_web_context_register_uri_scheme(context, "wails", (WebKitURISchemeRequestCallback)processURLRequest, NULL, NULL); //g_signal_connect(G_OBJECT(webview), "load-changed", G_CALLBACK(webview_load_changed_cb), NULL); @@ -376,6 +384,7 @@ GtkFileFilter* newFileFilter() { import "C" import ( "github.com/wailsapp/wails/v2/internal/frontend" + "github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/pkg/options" "strings" "unsafe" @@ -389,11 +398,15 @@ func gtkBool(input bool) C.gboolean { } type Window struct { - appoptions *options.App - debug bool - gtkWindow unsafe.Pointer - contentManager unsafe.Pointer - webview unsafe.Pointer + appoptions *options.App + debug bool + gtkWindow unsafe.Pointer + contentManager unsafe.Pointer + webview unsafe.Pointer + applicationMenu *menu.Menu + menubar *C.GtkWidget + vbox *C.GtkWidget + accels *C.GtkAccelGroup } func bool2Cint(value bool) C.int { @@ -414,6 +427,9 @@ func NewWindow(appoptions *options.App, debug bool) *Window { C.g_object_ref_sink(C.gpointer(gtkWindow)) result.gtkWindow = unsafe.Pointer(gtkWindow) + result.vbox = C.gtk_box_new(C.GTK_ORIENTATION_VERTICAL, 0) + C.gtk_container_add(result.asGTKContainer(), result.vbox) + result.contentManager = unsafe.Pointer(C.webkit_user_content_manager_new()) external := C.CString("external") defer C.free(unsafe.Pointer(external)) @@ -428,7 +444,6 @@ func NewWindow(appoptions *options.App, debug bool) *Window { if debug { C.devtoolsEnabled(unsafe.Pointer(webview), C.int(1)) - } // Setup window @@ -440,6 +455,9 @@ func NewWindow(appoptions *options.App, debug bool) *Window { result.SetMinSize(appoptions.MinWidth, appoptions.MinHeight) result.SetMaxSize(appoptions.MaxWidth, appoptions.MaxHeight) + // Menu + result.SetApplicationMenu(appoptions.Menu) + return result } @@ -451,6 +469,10 @@ func (w *Window) asGTKWindow() *C.GtkWindow { return C.GTKWINDOW(w.gtkWindow) } +func (w *Window) asGTKContainer() *C.GtkContainer { + return C.GTKCONTAINER(w.gtkWindow) +} + func (w *Window) cWebKitUserContentManager() *C.WebKitUserContentManager { return (*C.WebKitUserContentManager)(w.contentManager) } @@ -549,6 +571,8 @@ func (w *Window) UpdateApplicationMenu() { } func (w *Window) Run() { + C.gtk_box_pack_start(C.GTKBOX(unsafe.Pointer(w.vbox)), w.menubar, 0, 0, 0) + C.gtk_box_pack_start(C.GTKBOX(unsafe.Pointer(w.vbox)), C.GTKWIDGET(w.webview), 1, 1, 0) C.loadIndex(w.webview) C.gtk_widget_show_all(w.asGTKWidget()) w.Center()