5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-16 00:49:32 +08:00

Initial support for menus

This commit is contained in:
Lea Anthony 2020-11-24 19:43:25 +11:00
parent 810b3c7440
commit 6f218264ed
No known key found for this signature in database
GPG Key ID: 33DAF7BB90A58405
19 changed files with 906 additions and 28 deletions

View File

@ -3,6 +3,9 @@
package app
import (
"fmt"
goruntime "runtime"
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/ffenestri"
"github.com/wailsapp/wails/v2/internal/logger"
@ -11,6 +14,7 @@ import (
"github.com/wailsapp/wails/v2/internal/servicebus"
"github.com/wailsapp/wails/v2/internal/signal"
"github.com/wailsapp/wails/v2/internal/subsystem"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options"
)
@ -28,6 +32,7 @@ type App struct {
event *subsystem.Event
binding *subsystem.Binding
call *subsystem.Call
menu *subsystem.Menu
dispatcher *messagedispatcher.Dispatcher
// Indicates if the app is in debug mode
@ -128,6 +133,25 @@ func (a *App) Run() error {
a.event = event
a.event.Start()
// Start the menu subsystem
var platformMenu *menu.Menu
switch goruntime.GOOS {
case "darwin":
platformMenu = a.options.Mac.Menu
// case "linux":
// platformMenu = a.options.Linux.Menu
// case "windows":
// platformMenu = a.options.Windows.Menu
default:
return fmt.Errorf("unsupported OS: %s", goruntime.GOOS)
}
menu, err := subsystem.NewMenu(platformMenu, a.servicebus, a.logger)
if err != nil {
return err
}
a.menu = menu
a.menu.Start()
// Start the call subsystem
call, err := subsystem.NewCall(a.servicebus, a.logger, a.bindings.DB(), a.runtime.GoRuntime())
if err != nil {

View File

@ -20,6 +20,8 @@
#define GET_FRAME(receiver) ((CGRect(*)(id, SEL))objc_msgSend_stret)(receiver, s("frame"))
#define GET_BOUNDS(receiver) ((CGRect(*)(id, SEL))objc_msgSend_stret)(receiver, s("bounds"))
#define STREQ(a,b) strncmp(a, b, strlen(b)) == 0
#define ON_MAIN_THREAD(str) dispatch( ^{ str; } )
#define MAIN_WINDOW_CALL(str) msg(app->mainWindow, s((str)))
@ -49,7 +51,7 @@
#define NSEventModifierFlagCommand 1 << 20
#define NSEventModifierFlagOption 1 << 19
#define NSEventModifierFlagShift 1 << 17
// Unbelievably, if the user swaps their button preference
@ -156,6 +158,10 @@ struct Application {
int useToolBar;
int hideToolbarSeparator;
int windowBackgroundIsTranslucent;
// Menu
const char *menuAsJSON;
id menubar;
// User Data
char *HTML;
@ -189,6 +195,16 @@ void Debug(struct Application *app, const char *message, ... ) {
}
}
void Fatal(struct Application *app, const char *message, ... ) {
const char *temp = concat("LFFfenestri (C) | ", message);
va_list args;
va_start(args, message);
vsnprintf(logbuffer, MAXMESSAGE, temp, args);
app->sendMessageToBackend(&logbuffer[0]);
free((void*)temp);
va_end(args);
}
void TitlebarAppearsTransparent(struct Application* app) {
app->titlebarAppearsTransparent = 1;
}
@ -292,6 +308,15 @@ void messageHandler(id self, SEL cmd, id contentController, id message) {
}
}
// Callback for menu items
void menuItemPressed(id self, SEL cmd, id sender) {
const char *callbackID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue"));
printf("Got callback ID: %s\n", callbackID);
const char *message = concat("MC", callbackID);
messageFromWindowCallback(message);
free((void*)message);
}
// closeWindow is called when the close button is pressed
void closeWindow(id self, SEL cmd, id sender) {
printf("\n\n\ncloseWindow called!!!!\n\n\n");
@ -371,6 +396,8 @@ void* NewApplication(const char *title, int width, int height, int resizable, in
result->vibrancyLayer = NULL;
result->delegate = NULL;
// Menu
result->menuAsJSON = NULL;
result->titlebarAppearsTransparent = 0;
result->webviewIsTranparent = 0;
@ -745,6 +772,11 @@ void SetDebug(void *applicationPointer, int flag) {
debug = flag;
}
// SetMenu sets the initial menu for the application
void SetMenu(struct Application *app, const char *menuAsJSON) {
app->menuAsJSON = menuAsJSON;
}
void SetBindings(struct Application *app, const char *bindings) {
const char* temp = concat("window.wailsbindings = \"", bindings);
const char* jscall = concat(temp, "\";");
@ -832,6 +864,9 @@ void createDelegate(struct Application *app) {
class_addMethod(delegateClass, s("applicationShouldTerminateAfterLastWindowClosed:"), (IMP) yes, "c@:@");
class_addMethod(delegateClass, s("windowWillClose:"), (IMP) closeWindow, "v@:@");
// Menu Callbacks
class_addMethod(delegateClass, s("menuCallback:"), (IMP)menuItemPressed, "v@:@");
// Script handler
class_addMethod(delegateClass, s("userContentController:didReceiveScriptMessage:"), (IMP) messageHandler, "v@:@@");
objc_registerClassPair(delegateClass);
@ -901,70 +936,292 @@ id createMenuItemNoAutorelease( id title, const char *action, const char *key) {
}
id createMenu(id title) {
id menu = ALLOC("NSMenu");
msg(menu, s("initWithTitle:"), title);
msg(menu, s("autorelease"));
return menu;
id menu = ALLOC("NSMenu");
msg(menu, s("initWithTitle:"), title);
msg(menu, s("autorelease"));
return menu;
}
id addMenuItem(id menu, const char *title, const char *action, const char *key) {
id addMenuItem(id menu, const char *title, const char *action, const char *key, bool enabled) {
id item = createMenuItem(str(title), action, key);
msg(item, s("setEnabled:"), enabled);
msg(menu, s("addItem:"), item);
return item;
}
id addCallbackMenuItem(id menu, const char *title, const char *menuid, const char *key, bool enabled) {
id item = ALLOC("NSMenuItem");
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), menuid);
msg(item, s("setRepresentedObject:"), wrappedId);
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuCallback:"), str(key));
msg(item, s("setEnabled:"), enabled);
msg(item, s("autorelease"));
msg(menu, s("addItem:"), item);
return item;
}
void addSeparator(id menu) {
id item = msg(c("NSMenuItem"), s("separatorItem"));
msg(menu, s("addItem:"), item);
}
void addDefaultMenu() {
id menubar = createMenu(str(""));
id appName = msg(msg(c("NSProcessInfo"), s("processInfo")), s("processName"));
id appMenuItem = createMenuItemNoAutorelease(appName, NULL, "");
void createDefaultAppMenu(id parentMenu) {
// App Menu
id appName = msg(msg(c("NSProcessInfo"), s("processInfo")), s("processName"));
id appMenuItem = createMenuItemNoAutorelease(appName, NULL, "");
id appMenu = createMenu(appName);
msg(appMenuItem, s("setSubmenu:"), appMenu);
msg(menubar, s("addItem:"), appMenuItem);
msg(parentMenu, s("addItem:"), appMenuItem);
id title = msg(str("Hide "), s("stringByAppendingString:"), appName);
id item = createMenuItem(title, "hide:", "h");
msg(appMenu, s("addItem:"), item);
id hideOthers = addMenuItem(appMenu, "Hide Others", "hideOtherApplications:", "h");
id hideOthers = addMenuItem(appMenu, "Hide Others", "hideOtherApplications:", "h", TRUE);
msg(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
addMenuItem(appMenu, "Show All", "unhideAllApplications:", "");
addMenuItem(appMenu, "Show All", "unhideAllApplications:", "", TRUE);
addSeparator(appMenu);
title = msg(str("Quit "), s("stringByAppendingString:"), appName);
item = createMenuItem(title, "terminate:", "q");
msg(appMenu, s("addItem:"), item);
}
void createDefaultEditMenu(id parentMenu) {
// Edit Menu
id editMenuItem = createMenuItemNoAutorelease(str("Edit"), NULL, "");
id editMenu = createMenu(str("Edit"));
msg(editMenuItem, s("setSubmenu:"), editMenu);
msg(menubar, s("addItem:"), editMenuItem);
msg(parentMenu, s("addItem:"), editMenuItem);
addMenuItem(editMenu, "Undo", "undo:", "z");
addMenuItem(editMenu, "Redo", "redo:", "y");
addMenuItem(editMenu, "Undo", "undo:", "z", TRUE);
addMenuItem(editMenu, "Redo", "redo:", "y", TRUE);
addSeparator(editMenu);
addMenuItem(editMenu, "Cut", "cut:", "x");
addMenuItem(editMenu, "Copy", "copy:", "c");
addMenuItem(editMenu, "Paste", "paste:", "v");
addMenuItem(editMenu, "Select All", "selectAll:", "a");
addMenuItem(editMenu, "Cut", "cut:", "x", TRUE);
addMenuItem(editMenu, "Copy", "copy:", "c", TRUE);
addMenuItem(editMenu, "Paste", "paste:", "v", TRUE);
addMenuItem(editMenu, "Select All", "selectAll:", "a", TRUE);
}
void parseMenuRole(struct Application *app, id parentMenu, JsonNode *item) {
const char *roleName = item->string_;
if ( STREQ(roleName, "appMenu") ) {
createDefaultAppMenu(parentMenu);
return;
}
if ( STREQ(roleName, "editMenu")) {
createDefaultEditMenu(parentMenu);
return;
}
if ( STREQ(roleName, "hide")) {
addMenuItem(parentMenu, "Hide Window", "hide:", "h", TRUE);
return;
}
if ( STREQ(roleName, "hideothers")) {
id hideOthers = addMenuItem(parentMenu, "Hide Others", "hideOtherApplications:", "h", TRUE);
msg(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
return;
}
if ( STREQ(roleName, "unhide")) {
addMenuItem(parentMenu, "Show All", "unhideAllApplications:", "", TRUE);
return;
}
if ( STREQ(roleName, "front")) {
addMenuItem(parentMenu, "Bring All to Front", "arrangeInFront:", "", TRUE);
return;
}
if ( STREQ(roleName, "undo")) {
addMenuItem(parentMenu, "Undo", "undo:", "z", TRUE);
return;
}
if ( STREQ(roleName, "redo")) {
addMenuItem(parentMenu, "Redo", "redo:", "y", TRUE);
return;
}
if ( STREQ(roleName, "cut")) {
addMenuItem(parentMenu, "Cut", "cut:", "x", TRUE);
return;
}
if ( STREQ(roleName, "copy")) {
addMenuItem(parentMenu, "Copy", "copy:", "c", TRUE);
return;
}
if ( STREQ(roleName, "paste")) {
addMenuItem(parentMenu, "Paste", "paste:", "v", TRUE);
return;
}
if ( STREQ(roleName, "delete")) {
addMenuItem(parentMenu, "Delete", "delete:", "", TRUE);
return;
}
if( STREQ(roleName, "pasteandmatchstyle")) {
id pasteandmatchstyle = addMenuItem(parentMenu, "Paste and Match Style", "pasteandmatchstyle:", "v", TRUE);
msg(pasteandmatchstyle, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagShift | NSEventModifierFlagCommand));
}
if ( STREQ(roleName, "selectall")) {
addMenuItem(parentMenu, "Select All", "selectAll:", "a", TRUE);
return;
}
if ( STREQ(roleName, "minimize")) {
addMenuItem(parentMenu, "Minimize", "miniaturize:", "m", TRUE);
return;
}
if ( STREQ(roleName, "zoom")) {
addMenuItem(parentMenu, "Zoom", "performZoom:", "", TRUE);
return;
}
if ( STREQ(roleName, "quit")) {
addMenuItem(parentMenu, "Quit (More work TBD)", "terminate:", "q", TRUE);
return;
}
if ( STREQ(roleName, "togglefullscreen")) {
addMenuItem(parentMenu, "Toggle Full Screen", "toggleFullScreen:", "f", TRUE);
return;
}
}
const char* getJSONString(JsonNode *item, const char* key) {
// Get key
JsonNode *node = json_find_member(item, key);
const char *result = "";
if ( node != NULL && node->tag == JSON_STRING) {
result = node->string_;
}
return result;
}
bool getJSONBool(JsonNode *item, const char* key, bool *result) {
JsonNode *node = json_find_member(item, key);
if ( node != NULL && node->tag == JSON_BOOL) {
*result = node->bool_;
return true;
}
return false;
}
void parseNormalMenuItem(struct Application *app, id parentMenu, JsonNode *item) {
// Get the label
const char *label = getJSONString(item, "Label");
if ( label == NULL) {
label = "(empty)";
}
const char *menuid = getJSONString(item, "Id");
if ( menuid == NULL) {
menuid = "";
}
bool enabled = true;
getJSONBool(item, "Enabled", &enabled);
const char *accelerator = "";
printf("Parsing Normal Menu Item %s!!!\n", label);
addCallbackMenuItem(parentMenu, label, menuid, accelerator, enabled);
}
void parseMenuItem(struct Application *app, id parentMenu, JsonNode *item) {
// Get the role
JsonNode *role = json_find_member(item, "Role");
if( role != NULL ) {
printf("Parsing MENU ROLE %s!!!\n", role->string_);
parseMenuRole(app, parentMenu, role);
return;
}
// Check if this is a submenu
JsonNode *submenu = json_find_member(item, "SubMenu");
if( submenu != NULL ) {
printf("Parsing SUBMENU!!!\n");
// Get the label
JsonNode *menuNameNode = json_find_member(item, "Label");
const char *name = "";
if ( menuNameNode != NULL) {
name = menuNameNode->string_;
}
id thisMenuItem = createMenuItemNoAutorelease(str(name), NULL, "");
id thisMenu = createMenu(str(name));
msg(thisMenuItem, s("setSubmenu:"), thisMenu);
msg(parentMenu, s("addItem:"), thisMenuItem);
// Loop over submenu items
JsonNode *item;
json_foreach(item, submenu) {
// Get item label
parseMenuItem(app, thisMenu, item);
printf("Parsing submenu item for '%s'!!!\n", name);
}
return;
}
// Get the Type
JsonNode *type = json_find_member(item, "Type");
if( type != NULL ) {
if( STREQ(type->string_, "Normal")) {
parseNormalMenuItem(app, parentMenu, item);
return;
}
if ( STREQ(type->string_, "Separator")) {
addSeparator(parentMenu);
return;
}
return;
}
}
void parseMenu(struct Application *app, id parentMenu, JsonNode *menu) {
JsonNode *items = json_find_member(menu, "Items");
if( items == NULL ) {
// Parse error!
Fatal(app, "Unable to find Items:", app->menuAsJSON);
return;
}
// Iterate items
JsonNode *item;
json_foreach(item, items) {
// Get item label
parseMenuItem(app, parentMenu, item);
}
}
void parseMenuData(struct Application *app) {
// Create a new menu bar
id menubar = createMenu(str(""));
// Parse the menu json
JsonNode *menuData = json_decode(app->menuAsJSON);
if( menuData == NULL ) {
// Parse error!
Fatal(app, "Unable to parse Menu JSON:", app->menuAsJSON);
return;
}
parseMenu(app, menubar, menuData);
// Apply the menu bar
msg(msg(c("NSApplication"), s("sharedApplication")), s("setMainMenu:"), menubar);
}
void Run(struct Application *app, int argc, char **argv) {
processDecorations(app);
@ -1129,7 +1386,9 @@ void Run(struct Application *app, int argc, char **argv) {
msg(wkwebview, s("setValue:forKey:"), msg(c("NSNumber"), s("numberWithBool:"), 0), str("drawsBackground"));
}
addDefaultMenu(app);
if( app->menuAsJSON != NULL ) {
parseMenuData(app);
}
// Finally call run
Debug(app, "Run called");

View File

@ -14,10 +14,12 @@ extern void DisableFrame(void *);
extern void SetAppearance(void *, const char *);
extern void WebviewIsTransparent(void *);
extern void SetWindowBackgroundIsTranslucent(void *);
extern void SetMenu(void *, const char *);
*/
import "C"
import "encoding/json"
func (a *Application) processPlatformSettings() {
func (a *Application) processPlatformSettings() error {
mac := a.config.Mac
titlebar := mac.TitleBar
@ -64,4 +66,15 @@ func (a *Application) processPlatformSettings() {
if mac.WindowBackgroundIsTranslucent {
C.SetWindowBackgroundIsTranslucent(a.app)
}
// Process menu
if mac.Menu != nil {
menuJson, err := json.Marshal(mac.Menu)
if err != nil {
return err
}
C.SetMenu(a.app, a.string2CString(string(menuJson)))
}
return nil
}

View File

@ -0,0 +1,43 @@
package message
import (
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
)
// MenuOnMessage is used to emit listener registration requests
// on the service bus
type MenuOnMessage struct {
// MenuID is the id of the menu item we are interested in
MenuID string
// Callback is called when the menu is clicked
Callback func(*menu.MenuItem)
}
// menuMessageParser does what it says on the tin!
func menuMessageParser(message string) (*parsedMessage, error) {
// Sanity check: Menu messages must be at least 2 bytes
if len(message) < 3 {
return nil, fmt.Errorf("event message was an invalid length")
}
var topic string
var data interface{}
// Switch the message type
switch message[1] {
case 'C':
callbackid := message[2:]
topic = "menu:clicked"
data = callbackid
default:
return nil, fmt.Errorf("invalid menu message: %s", message)
}
// Create a new parsed message struct
parsedMessage := &parsedMessage{Topic: topic, Data: data}
return parsedMessage, nil
}

View File

@ -18,6 +18,7 @@ var messageParsers = map[byte]func(string) (*parsedMessage, error){
'W': windowMessageParser,
'D': dialogMessageParser,
'S': systemMessageParser,
'M': menuMessageParser,
}
// Parse will attempt to parse the given message

View File

@ -0,0 +1,31 @@
package runtime
import (
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
"github.com/wailsapp/wails/v2/internal/servicebus"
"github.com/wailsapp/wails/v2/pkg/menu"
)
// Menu defines all Menu related operations
type Menu interface {
On(menuID string, callback func(*menu.MenuItem))
}
type menuRuntime struct {
bus *servicebus.ServiceBus
}
// newMenu creates a new Menu struct
func newMenu(bus *servicebus.ServiceBus) Menu {
return &menuRuntime{
bus: bus,
}
}
// On registers a listener for a particular event
func (m *menuRuntime) On(menuID string, callback func(*menu.MenuItem)) {
m.bus.Publish("menu:on", &message.MenuOnMessage{
MenuID: menuID,
Callback: callback,
})
}

View File

@ -9,6 +9,7 @@ type Runtime struct {
Window Window
Dialog Dialog
System System
Menu Menu
Store *StoreProvider
Log Log
bus *servicebus.ServiceBus
@ -22,6 +23,7 @@ func New(serviceBus *servicebus.ServiceBus) *Runtime {
Window: newWindow(serviceBus),
Dialog: newDialog(serviceBus),
System: newSystem(serviceBus),
Menu: newMenu(serviceBus),
Log: newLog(serviceBus),
bus: serviceBus,
}

View File

@ -0,0 +1,171 @@
package subsystem
import (
"strings"
"sync"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
"github.com/wailsapp/wails/v2/internal/servicebus"
"github.com/wailsapp/wails/v2/pkg/menu"
)
// eventListener holds a callback function which is invoked when
// the event listened for is emitted. It has a counter which indicates
// how the total number of events it is interested in. A value of zero
// means it does not expire (default).
// type eventListener struct {
// callback func(...interface{}) // Function to call with emitted event data
// counter int // The number of times this callback may be called. -1 = infinite
// delete bool // Flag to indicate that this listener should be deleted
// }
// Menu is the subsystem that handles the operation of menus. It manages all service bus messages
// starting with "menu".
type Menu struct {
quitChannel <-chan *servicebus.Message
menuChannel <-chan *servicebus.Message
running bool
// Event listeners
listeners map[string][]func(*menu.MenuItem)
menuItems map[string]*menu.MenuItem
notifyLock sync.RWMutex
// logger
logger logger.CustomLogger
}
// NewMenu creates a new menu subsystem
func NewMenu(initialMenu *menu.Menu, bus *servicebus.ServiceBus, logger *logger.Logger) (*Menu, error) {
// Register quit channel
quitChannel, err := bus.Subscribe("quit")
if err != nil {
return nil, err
}
// Subscribe to menu messages
menuChannel, err := bus.Subscribe("menu")
if err != nil {
return nil, err
}
result := &Menu{
quitChannel: quitChannel,
menuChannel: menuChannel,
logger: logger.CustomLogger("Menu Subsystem"),
listeners: make(map[string][]func(*menu.MenuItem)),
menuItems: make(map[string]*menu.MenuItem),
}
// Build up list of item/id pairs
result.processMenu(initialMenu)
return result, nil
}
// Start the subsystem
func (m *Menu) Start() error {
m.logger.Trace("Starting")
m.running = true
// Spin off a go routine
go func() {
for m.running {
select {
case <-m.quitChannel:
m.running = false
break
case menuMessage := <-m.menuChannel:
splitTopic := strings.Split(menuMessage.Topic(), ":")
menuMessageType := splitTopic[1]
switch menuMessageType {
case "clicked":
if len(splitTopic) != 2 {
m.logger.Error("Received clicked message with invalid topic format. Expected 2 sections in topic, got %s", splitTopic)
continue
}
m.logger.Trace("Got Menu clicked Message: %s %+v", menuMessage.Topic(), menuMessage.Data())
callbackID := menuMessage.Data().(string)
m.notifyListeners(callbackID)
case "on":
listenerDetails := menuMessage.Data().(*message.MenuOnMessage)
id := listenerDetails.MenuID
// Check we have a menu with that id
if m.menuItems[id] == nil {
m.logger.Error("cannot register listener for unknown menu id '%s'", id)
continue
}
// We do! Append the callback
m.listeners[id] = append(m.listeners[id], listenerDetails.Callback)
default:
m.logger.Error("unknown menu message: %+v", menuMessage)
}
}
}
// Call shutdown
m.shutdown()
}()
return nil
}
func (m *Menu) processMenu(menu *menu.Menu) {
for _, item := range menu.Items {
m.processMenuItem(item)
}
}
func (m *Menu) processMenuItem(item *menu.MenuItem) {
if item.SubMenu != nil {
for _, submenuitem := range item.SubMenu {
m.processMenuItem(submenuitem)
}
return
}
if item.Id != "" {
if m.menuItems[item.Id] != nil {
m.logger.Error("Menu id '%s' is used by multiple menu items: %s %s", m.menuItems[item.Id].Label, item.Label)
return
}
m.menuItems[item.Id] = item
}
}
// Notifies listeners that the given menu was clicked
func (m *Menu) notifyListeners(menuid string) {
// Get the menu item
menuItem := m.menuItems[menuid]
if menuItem == nil {
m.logger.Trace("Cannot process menuid %s - unknown", menuid)
return
}
// Get list of menu listeners
listeners := m.listeners[menuid]
if listeners == nil {
m.logger.Trace("No listeners for %s", menuid)
return
}
// Lock the listeners
m.notifyLock.Lock()
// Callback in goroutine
for _, listener := range listeners {
go listener(menuItem)
}
// Unlock
m.notifyLock.Unlock()
}
func (m *Menu) shutdown() {
m.logger.Trace("Shutdown")
}

10
v2/pkg/menu/mac.go Normal file
View File

@ -0,0 +1,10 @@
package menu
// DefaultMacMenu returns a default menu including the default
// Application and Edit menus. Use `.Append()` to add to it.
func DefaultMacMenu() *Menu {
return NewMenuFromItems(
AppMenu(),
EditMenu(),
)
}

24
v2/pkg/menu/menu.go Normal file
View File

@ -0,0 +1,24 @@
package menu
type Menu struct {
Items []*MenuItem
}
func NewMenu() *Menu {
return &Menu{}
}
func (m *Menu) Append(item *MenuItem) {
m.Items = append(m.Items, item)
}
func NewMenuFromItems(first *MenuItem, rest ...*MenuItem) *Menu {
var result = NewMenu()
result.Append(first)
for _, item := range rest {
result.Append(item)
}
return result
}

30
v2/pkg/menu/menuitem.go Normal file
View File

@ -0,0 +1,30 @@
package menu
type MenuItem struct {
Id string `json:"Id,omitempty"`
Label string
Role Role `json:"Role,omitempty"`
Accelerator string `json:"Accelerator,omitempty"`
Type Type
Enabled bool
Visible bool
Checked bool
SubMenu []*MenuItem `json:"SubMenu,omitempty"`
}
func Text(label string, id string) *MenuItem {
return &MenuItem{
Id: id,
Label: label,
Type: NormalType,
Enabled: true,
Visible: true,
}
}
// Separator provides a menu separator
func Separator() *MenuItem {
return &MenuItem{
Type: SeparatorType,
}
}

204
v2/pkg/menu/menuroles.go Normal file
View File

@ -0,0 +1,204 @@
// Package menu provides all the functions and structs related to menus in a Wails application.
// Heavily inspired by Electron (c) 2013-2020 Github Inc.
// Electron License: https://github.com/electron/electron/blob/master/LICENSE
package menu
type Role string
const (
AboutRole Role = "about"
UndoRole Role = "undo"
RedoRole Role = "redo"
CutRole Role = "cut"
CopyRole Role = "copy"
PasteRole Role = "paste"
PasteAndMatchStyleRole Role = "pasteAndMatchStyle"
SelectAllRole Role = "selectAll"
DeleteRole Role = "delete"
MinimizeRole Role = "minimize"
QuitRole Role = "quit"
TogglefullscreenRole Role = "togglefullscreen"
FileMenuRole Role = "fileMenu"
EditMenuRole Role = "editMenu"
ViewMenuRole Role = "viewMenu"
WindowMenuRole Role = "windowMenu"
AppMenuRole Role = "appMenu"
HideRole Role = "hide"
HideOthersRole Role = "hideOthers"
UnhideRole Role = "unhide"
FrontRole Role = "front"
ZoomRole Role = "zoom"
WindowSubMenuRole Role = "windowSubMenu"
HelpSubMenuRole Role = "helpSubMenu"
SeparatorItemRole Role = "separatorItem"
)
// About provides a MenuItem with the About role
func About() *MenuItem {
return &MenuItem{
Role: AboutRole,
}
}
// Undo provides a MenuItem with the Undo role
func Undo() *MenuItem {
return &MenuItem{
Role: UndoRole,
}
}
// Redo provides a MenuItem with the Redo role
func Redo() *MenuItem {
return &MenuItem{
Role: RedoRole,
}
}
// Cut provides a MenuItem with the Cut role
func Cut() *MenuItem {
return &MenuItem{
Role: CutRole,
}
}
// Copy provides a MenuItem with the Copy role
func Copy() *MenuItem {
return &MenuItem{
Role: CopyRole,
}
}
// Paste provides a MenuItem with the Paste role
func Paste() *MenuItem {
return &MenuItem{
Role: PasteRole,
}
}
// PasteAndMatchStyle provides a MenuItem with the PasteAndMatchStyle role
func PasteAndMatchStyle() *MenuItem {
return &MenuItem{
Role: PasteAndMatchStyleRole,
}
}
// SelectAll provides a MenuItem with the SelectAll role
func SelectAll() *MenuItem {
return &MenuItem{
Role: SelectAllRole,
}
}
// Delete provides a MenuItem with the Delete role
func Delete() *MenuItem {
return &MenuItem{
Role: DeleteRole,
}
}
// Minimize provides a MenuItem with the Minimize role
func Minimize() *MenuItem {
return &MenuItem{
Role: MinimizeRole,
}
}
// Quit provides a MenuItem with the Quit role
func Quit() *MenuItem {
return &MenuItem{
Role: QuitRole,
}
}
// Togglefullscreen provides a MenuItem with the Togglefullscreen role
func Togglefullscreen() *MenuItem {
return &MenuItem{
Role: TogglefullscreenRole,
}
}
// FileMenu provides a MenuItem with the whole default "File" menu (Close / Quit)
func FileMenu() *MenuItem {
return &MenuItem{
Role: FileMenuRole,
}
}
// EditMenu provides a MenuItem with the whole default "Edit" menu (Undo, Copy, etc.).
func EditMenu() *MenuItem {
return &MenuItem{
Role: EditMenuRole,
}
}
// ViewMenu provides a MenuItem with the whole default "View" menu (Reload, Toggle Developer Tools, etc.)
func ViewMenu() *MenuItem {
return &MenuItem{
Role: ViewMenuRole,
}
}
// WindowMenu provides a MenuItem with the whole default "Window" menu (Minimize, Zoom, etc.).
func WindowMenu() *MenuItem {
return &MenuItem{
Role: WindowMenuRole,
}
}
// These roles are Mac only
// AppMenu provides a MenuItem with the whole default "App" menu (About, Services, etc.)
func AppMenu() *MenuItem {
return &MenuItem{
Role: AppMenuRole,
}
}
// Hide provides a MenuItem that maps to the hide action.
func Hide() *MenuItem {
return &MenuItem{
Role: HideRole,
}
}
// HideOthers provides a MenuItem that maps to the hideOtherApplications action.
func HideOthers() *MenuItem {
return &MenuItem{
Role: HideOthersRole,
}
}
// Unhide provides a MenuItem that maps to the unhideAllApplications action.
func Unhide() *MenuItem {
return &MenuItem{
Role: UnhideRole,
}
}
// Front provides a MenuItem that maps to the arrangeInFront action.
func Front() *MenuItem {
return &MenuItem{
Role: FrontRole,
}
}
// Zoom provides a MenuItem that maps to the performZoom action.
func Zoom() *MenuItem {
return &MenuItem{
Role: ZoomRole,
}
}
// WindowSubMenu provides a MenuItem with the "Window" submenu.
func WindowSubMenu() *MenuItem {
return &MenuItem{
Role: WindowSubMenuRole,
}
}
// HelpSubMenu provides a MenuItem with the "Help" submenu.
func HelpSubMenu() *MenuItem {
return &MenuItem{
Role: HelpSubMenuRole,
}
}

10
v2/pkg/menu/submenu.go Normal file
View File

@ -0,0 +1,10 @@
package menu
// SubMenu creates a new submenu which may be added to other
// menus
func SubMenu(label string, items []*MenuItem) *MenuItem {
return &MenuItem{
Label: label,
SubMenu: items,
}
}

17
v2/pkg/menu/type.go Normal file
View File

@ -0,0 +1,17 @@
package menu
// Type of the menu item
type Type string
const (
// NormalType is the Normal menuitem type
NormalType Type = "Normal"
// SeparatorType is the Separator menuitem type
SeparatorType Type = "Separator"
// SubmenuType is the Submenu menuitem type
SubmenuType Type = "Submenu"
// CheckboxType is the Checkbox menuitem type
CheckboxType Type = "Checkbox"
// RadioType is the Radio menuitem type
RadioType Type = "Radio"
)

View File

@ -2,6 +2,7 @@ package options
import (
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options/mac"
)
@ -17,6 +18,7 @@ var Default = &App{
Appearance: mac.DefaultAppearance,
WebviewIsTransparent: false,
WindowBackgroundIsTranslucent: false,
Menu: menu.DefaultMacMenu(),
},
Logger: logger.NewDefaultLogger(),
LogLevel: logger.INFO,

View File

@ -1,9 +1,12 @@
package mac
// Options are options speific to Mac
import "github.com/wailsapp/wails/v2/pkg/menu"
// Options are options specific to Mac
type Options struct {
TitleBar *TitleBar
Appearance AppearanceType
WebviewIsTransparent bool
WindowBackgroundIsTranslucent bool
Menu *menu.Menu
}

View File

@ -1,7 +1,10 @@
package main
import (
"fmt"
wails "github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options"
)
@ -14,6 +17,11 @@ type Dialog struct {
func (l *Dialog) WailsInit(runtime *wails.Runtime) error {
// Perform your setup here
l.runtime = runtime
// Setup Menu Listeners
l.runtime.Menu.On("hello", func(m *menu.MenuItem) {
fmt.Printf("The '%s' menu was clicked\n", m.Label)
})
return nil
}

View File

@ -3,12 +3,38 @@ package main
import (
wails "github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/mac"
)
func main() {
// Create menu
myMenu := menu.DefaultMacMenu()
windowMenu := menu.SubMenu("Test", []*menu.MenuItem{
menu.Togglefullscreen(),
menu.Minimize(),
menu.Zoom(),
menu.Separator(),
menu.Copy(),
menu.Cut(),
menu.Delete(),
menu.Separator(),
menu.Front(),
menu.SubMenu("Test Submenu", []*menu.MenuItem{
menu.Text("Hi!", "hello"), // Label = "Hi!", ID= "hello"
}),
})
myMenu.Append(windowMenu)
// Create application with options
app := wails.CreateAppWithOptions(&options.App{
Title: "Kitchen Sink",
@ -21,6 +47,7 @@ func main() {
WindowBackgroundIsTranslucent: true,
// Comment out line below to see Window.SetTitle() work
TitleBar: mac.TitleBarHiddenInset(),
Menu: myMenu,
},
LogLevel: logger.TRACE,
})

View File

@ -17,7 +17,6 @@ func (w *Window) WailsInit(runtime *wails.Runtime) error {
}
func (w *Window) SetTitle(title string) {
println("In SetTitle:", title)
w.runtime.Window.SetTitle(title)
}