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:
parent
810b3c7440
commit
6f218264ed
@ -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 {
|
||||
|
@ -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");
|
||||
|
@ -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
|
||||
}
|
||||
|
43
v2/internal/messagedispatcher/message/menu.go
Normal file
43
v2/internal/messagedispatcher/message/menu.go
Normal 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
|
||||
}
|
@ -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
|
||||
|
31
v2/internal/runtime/menu.go
Normal file
31
v2/internal/runtime/menu.go
Normal 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,
|
||||
})
|
||||
}
|
@ -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,
|
||||
}
|
||||
|
171
v2/internal/subsystem/menu.go
Normal file
171
v2/internal/subsystem/menu.go
Normal 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
10
v2/pkg/menu/mac.go
Normal 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
24
v2/pkg/menu/menu.go
Normal 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
30
v2/pkg/menu/menuitem.go
Normal 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
204
v2/pkg/menu/menuroles.go
Normal 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
10
v2/pkg/menu/submenu.go
Normal 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
17
v2/pkg/menu/type.go
Normal 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"
|
||||
)
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user