mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-02 23:51:44 +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:
parent
e713c439f0
commit
642fa42f25
73
v2/internal/frontend/desktop/linux/gtk.go
Normal file
73
v2/internal/frontend/desktop/linux/gtk.go
Normal 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})
|
||||
}
|
||||
}
|
103
v2/internal/frontend/desktop/linux/keys.go
Normal file
103
v2/internal/frontend/desktop/linux/keys.go
Normal 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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
@ -394,6 +403,10 @@ type Window struct {
|
||||
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()
|
||||
|
Loading…
Reference in New Issue
Block a user