5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 09:50:30 +08:00

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
This commit is contained in:
Lea Anthony 2022-01-29 07:19:14 +11:00 committed by GitHub
parent e713c439f0
commit 642fa42f25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 357 additions and 9 deletions

View File

@ -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})
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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()