Support for dynamic menus.

Fixed bug in accelerator handling
This commit is contained in:
Lea Anthony
2020-12-04 15:04:51 +11:00
parent a3e50e760e
commit a081c1e498
14 changed files with 1269 additions and 1125 deletions

View File

@@ -78,11 +78,11 @@ func CreateApp(options *options.App) *App {
func (a *App) Run() error {
// Setup signal handler
signal, err := signal.NewManager(a.servicebus, a.logger)
signalsubsystem, err := signal.NewManager(a.servicebus, a.logger)
if err != nil {
return err
}
a.signal = signal
a.signal = signalsubsystem
a.signal.Start()
// Start the service bus
@@ -90,11 +90,12 @@ func (a *App) Run() error {
a.servicebus.Start()
// Start the runtime
runtime, err := subsystem.NewRuntime(a.servicebus, a.logger)
runtimesubsystem, err := subsystem.NewRuntime(a.servicebus, a.logger,
a.options.Mac.Menu)
if err != nil {
return err
}
a.runtime = runtime
a.runtime = runtimesubsystem
a.runtime.Start()
// Application Stores
@@ -102,11 +103,12 @@ func (a *App) Run() error {
a.appconfigStore = a.runtime.GoRuntime().Store.New("wails:appconfig", a.options)
// Start the binding subsystem
binding, err := subsystem.NewBinding(a.servicebus, a.logger, a.bindings, a.runtime.GoRuntime())
bindingsubsystem, err := subsystem.NewBinding(a.servicebus, a.logger,
a.bindings, a.runtime.GoRuntime())
if err != nil {
return err
}
a.binding = binding
a.binding = bindingsubsystem
a.binding.Start()
// Start the logging subsystem
@@ -145,11 +147,11 @@ func (a *App) Run() error {
default:
return fmt.Errorf("unsupported OS: %s", goruntime.GOOS)
}
menu, err := subsystem.NewMenu(platformMenu, a.servicebus, a.logger)
menusubsystem, err := subsystem.NewMenu(platformMenu, a.servicebus, a.logger)
if err != nil {
return err
}
a.menu = menu
a.menu = menusubsystem
a.menu.Start()
// Start the call subsystem

View File

@@ -32,4 +32,5 @@ extern void DisableFrame(void *app);
extern void OpenDialog(void *appPointer, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int allowFiles, int allowDirs, int allowMultiple, int showHiddenFiles, int canCreateDirectories, int resolvesAliases, int treatPackagesAsDirectories);
extern void SaveDialog(void *appPointer, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int showHiddenFiles, int canCreateDirectories, int treatPackagesAsDirectories);
extern void DarkModeEnabled(void *appPointer, char *callbackID);
extern void UpdateMenu(void *app, char *menuAsJSON);
#endif

View File

@@ -12,6 +12,8 @@ package ffenestri
import "C"
import (
"encoding/json"
"github.com/wailsapp/wails/v2/pkg/menu"
"strconv"
"github.com/wailsapp/wails/v2/internal/logger"
@@ -154,3 +156,19 @@ func (c *Client) SaveDialog(dialogOptions *options.SaveDialog, callbackID string
func (c *Client) DarkModeEnabled(callbackID string) {
C.DarkModeEnabled(c.app.app, c.app.string2CString(callbackID))
}
func (c *Client) UpdateMenu(menu *menu.Menu) {
// Guard against nil menus
if menu == nil {
return
}
// Process the menu
processedMenu := NewProcessedMenu(menu)
menuJSON, err := json.Marshal(processedMenu)
if err != nil {
c.app.logger.Error("Error processing updated Menu: %s", err.Error())
return
}
C.UpdateMenu(c.app.app, c.app.string2CString(string(menuJSON)))
}

File diff suppressed because it is too large Load Diff

View File

@@ -19,8 +19,6 @@ extern void SetMenu(void *, const char *);
import "C"
import (
"encoding/json"
"github.com/wailsapp/wails/v2/pkg/menu"
)
func (a *Application) processPlatformSettings() error {
@@ -106,80 +104,3 @@ func (a *Application) processPlatformSettings() error {
return nil
}
// ProcessedMenu is the original menu with the addition
// of radio groups extracted from the menu data
type ProcessedMenu struct {
Menu *menu.Menu
RadioGroups []*RadioGroup
currentRadioGroup []string
}
// RadioGroup holds all the members of the same radio group
type RadioGroup struct {
Members []string
Length int
}
// NewProcessedMenu processed the given menu and returns
// the original menu with the extracted radio groups
func NewProcessedMenu(menu *menu.Menu) *ProcessedMenu {
result := &ProcessedMenu{
Menu: menu,
RadioGroups: []*RadioGroup{},
currentRadioGroup: []string{},
}
result.processMenu()
return result
}
func (p *ProcessedMenu) processMenu() {
// Loop over top level menus
for _, item := range p.Menu.Items {
// Process MenuItem
p.processMenuItem(item)
}
p.finaliseRadioGroup()
}
func (p *ProcessedMenu) processMenuItem(item *menu.MenuItem) {
switch item.Type {
// We need to recurse submenus
case menu.SubmenuType:
// Finalise any current radio groups as they don't trickle down to submenus
p.finaliseRadioGroup()
// Process each submenu item
for _, subitem := range item.SubMenu {
p.processMenuItem(subitem)
}
case menu.RadioType:
// Add the item to the radio group
p.currentRadioGroup = append(p.currentRadioGroup, item.ID)
default:
p.finaliseRadioGroup()
}
}
func (p *ProcessedMenu) finaliseRadioGroup() {
// If we were processing a radio group, fix up the references
if len(p.currentRadioGroup) > 0 {
// Create new radiogroup
group := &RadioGroup{
Members: p.currentRadioGroup,
Length: len(p.currentRadioGroup),
}
p.RadioGroups = append(p.RadioGroups, group)
// Empty the radio group
p.currentRadioGroup = []string{}
}
}

View File

@@ -0,0 +1,80 @@
package ffenestri
import "github.com/wailsapp/wails/v2/pkg/menu"
// ProcessedMenu is the original menu with the addition
// of radio groups extracted from the menu data
type ProcessedMenu struct {
Menu *menu.Menu
RadioGroups []*RadioGroup
currentRadioGroup []string
}
// RadioGroup holds all the members of the same radio group
type RadioGroup struct {
Members []string
Length int
}
// NewProcessedMenu processed the given menu and returns
// the original menu with the extracted radio groups
func NewProcessedMenu(menu *menu.Menu) *ProcessedMenu {
result := &ProcessedMenu{
Menu: menu,
RadioGroups: []*RadioGroup{},
currentRadioGroup: []string{},
}
result.processMenu()
return result
}
func (p *ProcessedMenu) processMenu() {
// Loop over top level menus
for _, item := range p.Menu.Items {
// Process MenuItem
p.processMenuItem(item)
}
p.finaliseRadioGroup()
}
func (p *ProcessedMenu) processMenuItem(item *menu.MenuItem) {
switch item.Type {
// We need to recurse submenus
case menu.SubmenuType:
// Finalise any current radio groups as they don't trickle down to submenus
p.finaliseRadioGroup()
// Process each submenu item
for _, subitem := range item.SubMenu {
p.processMenuItem(subitem)
}
case menu.RadioType:
// Add the item to the radio group
p.currentRadioGroup = append(p.currentRadioGroup, item.ID)
default:
p.finaliseRadioGroup()
}
}
func (p *ProcessedMenu) finaliseRadioGroup() {
// If we were processing a radio group, fix up the references
if len(p.currentRadioGroup) > 0 {
// Create new radiogroup
group := &RadioGroup{
Members: p.currentRadioGroup,
Length: len(p.currentRadioGroup),
}
p.RadioGroups = append(p.RadioGroups, group)
// Empty the radio group
p.currentRadioGroup = []string{}
}
}

View File

@@ -2,6 +2,7 @@ package messagedispatcher
import (
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
@@ -30,6 +31,7 @@ type Client interface {
WindowUnFullscreen()
WindowSetColour(colour int)
DarkModeEnabled(callbackID string)
UpdateMenu(menu *menu.Menu)
}
// DispatchClient is what the frontends use to interface with the
@@ -73,14 +75,6 @@ func (d *DispatchClient) DispatchMessage(incomingMessage string) {
d.logger.Trace("I got a parsedMessage: %+v", parsedMessage)
// Check error
if err != nil {
d.logger.Error(err.Error())
// Hrm... what do we do with this?
d.bus.PublishForTarget("generic:message", incomingMessage, d.id)
return
}
// Publish the parsed message
d.bus.PublishForTarget(parsedMessage.Topic, parsedMessage.Data, d.id)

View File

@@ -2,6 +2,7 @@ package messagedispatcher
import (
"encoding/json"
"github.com/wailsapp/wails/v2/pkg/menu"
"strconv"
"strings"
"sync"
@@ -22,6 +23,7 @@ type Dispatcher struct {
windowChannel <-chan *servicebus.Message
dialogChannel <-chan *servicebus.Message
systemChannel <-chan *servicebus.Message
menuChannel <-chan *servicebus.Message
running bool
servicebus *servicebus.ServiceBus
@@ -69,6 +71,11 @@ func New(servicebus *servicebus.ServiceBus, logger *logger.Logger) (*Dispatcher,
return nil, err
}
menuChannel, err := servicebus.Subscribe("menu:")
if err != nil {
return nil, err
}
result := &Dispatcher{
servicebus: servicebus,
eventChannel: eventChannel,
@@ -79,6 +86,7 @@ func New(servicebus *servicebus.ServiceBus, logger *logger.Logger) (*Dispatcher,
windowChannel: windowChannel,
dialogChannel: dialogChannel,
systemChannel: systemChannel,
menuChannel: menuChannel,
}
return result, nil
@@ -108,6 +116,8 @@ func (d *Dispatcher) Start() error {
d.processDialogMessage(dialogMessage)
case systemMessage := <-d.systemChannel:
d.processSystemMessage(systemMessage)
case menuMessage := <-d.menuChannel:
d.processMenuMessage(menuMessage)
}
}
@@ -177,6 +187,7 @@ func (d *Dispatcher) processCallResult(result *servicebus.Message) {
if client == nil {
// This is fatal - unknown target!
d.logger.Fatal("Unknown target for call result: %+v", result)
return
}
d.logger.Trace("Sending message to client %s: R%s", target, result.Data().(string))
@@ -397,3 +408,31 @@ func (d *Dispatcher) processDialogMessage(result *servicebus.Message) {
}
}
func (d *Dispatcher) processMenuMessage(result *servicebus.Message) {
splitTopic := strings.Split(result.Topic(), ":")
if len(splitTopic) < 2 {
d.logger.Error("Invalid menu message : %#v", result.Data())
return
}
command := splitTopic[1]
switch command {
case "update":
updatedMenu, ok := result.Data().(*menu.Menu)
if !ok {
d.logger.Error("Invalid data for 'dialog:select:open' : %#v", result.Data())
return
}
// TODO: Work out what we mean in a multi window environment...
// For now we will just pick the first one
for _, client := range d.clients {
client.frontend.UpdateMenu(updatedMenu)
}
default:
d.logger.Error("Unknown dialog command: %s", command)
}
}

View File

@@ -9,16 +9,19 @@ import (
// Menu defines all Menu related operations
type Menu interface {
On(menuID string, callback func(*menu.MenuItem))
Update()
}
type menuRuntime struct {
bus *servicebus.ServiceBus
bus *servicebus.ServiceBus
menu *menu.Menu
}
// newMenu creates a new Menu struct
func newMenu(bus *servicebus.ServiceBus) Menu {
func newMenu(bus *servicebus.ServiceBus, menu *menu.Menu) Menu {
return &menuRuntime{
bus: bus,
bus: bus,
menu: menu,
}
}
@@ -29,3 +32,7 @@ func (m *menuRuntime) On(menuID string, callback func(*menu.MenuItem)) {
Callback: callback,
})
}
func (m *menuRuntime) Update() {
m.bus.Publish("menu:update", m.menu)
}

View File

@@ -1,6 +1,9 @@
package runtime
import "github.com/wailsapp/wails/v2/internal/servicebus"
import (
"github.com/wailsapp/wails/v2/internal/servicebus"
"github.com/wailsapp/wails/v2/pkg/menu"
)
// Runtime is a means for the user to interact with the application at runtime
type Runtime struct {
@@ -16,14 +19,14 @@ type Runtime struct {
}
// New creates a new runtime
func New(serviceBus *servicebus.ServiceBus) *Runtime {
func New(serviceBus *servicebus.ServiceBus, menu *menu.Menu) *Runtime {
result := &Runtime{
Browser: newBrowser(),
Events: newEvents(serviceBus),
Window: newWindow(serviceBus),
Dialog: newDialog(serviceBus),
System: newSystem(serviceBus),
Menu: newMenu(serviceBus),
Menu: newMenu(serviceBus, menu),
Log: newLog(serviceBus),
bus: serviceBus,
}

View File

@@ -34,10 +34,13 @@ type Menu struct {
// logger
logger logger.CustomLogger
// The application menu
applicationMenu *menu.Menu
}
// NewMenu creates a new menu subsystem
func NewMenu(initialMenu *menu.Menu, bus *servicebus.ServiceBus, logger *logger.Logger) (*Menu, error) {
func NewMenu(applicationMenu *menu.Menu, bus *servicebus.ServiceBus, logger *logger.Logger) (*Menu, error) {
// Register quit channel
quitChannel, err := bus.Subscribe("quit")
@@ -52,15 +55,16 @@ func NewMenu(initialMenu *menu.Menu, bus *servicebus.ServiceBus, logger *logger.
}
result := &Menu{
quitChannel: quitChannel,
menuChannel: menuChannel,
logger: logger.CustomLogger("Menu Subsystem"),
listeners: make(map[string][]func(*menu.MenuItem)),
menuItems: make(map[string]*menu.MenuItem),
quitChannel: quitChannel,
menuChannel: menuChannel,
logger: logger.CustomLogger("Menu Subsystem"),
listeners: make(map[string][]func(*menu.MenuItem)),
menuItems: make(map[string]*menu.MenuItem),
applicationMenu: applicationMenu,
}
// Build up list of item/id pairs
result.processMenu(initialMenu)
result.processMenu(applicationMenu)
return result, nil
}

View File

@@ -2,6 +2,7 @@ package subsystem
import (
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
"strings"
"github.com/wailsapp/wails/v2/internal/logger"
@@ -23,7 +24,7 @@ type Runtime struct {
}
// NewRuntime creates a new runtime subsystem
func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger) (*Runtime, error) {
func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger, menu *menu.Menu) (*Runtime, error) {
// Register quit channel
quitChannel, err := bus.Subscribe("quit")
@@ -41,7 +42,7 @@ func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger) (*Runtime, er
quitChannel: quitChannel,
runtimeChannel: runtimeChannel,
logger: logger.CustomLogger("Runtime Subsystem"),
runtime: runtime.New(bus),
runtime: runtime.New(bus, menu),
}
return result, nil
@@ -75,7 +76,8 @@ func (r *Runtime) Start() error {
case "browser":
err = r.processBrowserMessage(method, runtimeMessage.Data())
default:
fmt.Errorf("unknown runtime message: %+v", runtimeMessage)
err = fmt.Errorf("unknown runtime message: %+v",
runtimeMessage)
}
// If we had an error, log it

View File

@@ -20,6 +20,27 @@ type MenuItem struct {
Checked bool
// Submenu contains a list of menu items that will be shown as a submenu
SubMenu []*MenuItem `json:"SubMenu,omitempty"`
// This holds the menu item's parent.
parent *MenuItem
}
// Parent returns the parent of the menu item.
// If it is a top level menu then it returns nil.
func (m *MenuItem) Parent() *MenuItem {
return m.parent
}
// Append will attempt to append the given menu item to
// this item's submenu items. If this menu item is not a
// submenu, then this method will not add the item and
// simply return false.
func (m *MenuItem) Append(item *MenuItem) bool {
if m.Type != SubmenuType {
return false
}
m.SubMenu = append(m.SubMenu, item)
return true
}
// Text is a helper to create basic Text menu items
@@ -78,9 +99,16 @@ func CheckboxWithAccelerator(label string, id string, checked bool, accelerator
// SubMenu is a helper to create Submenus
func SubMenu(label string, items []*MenuItem) *MenuItem {
return &MenuItem{
result := &MenuItem{
Label: label,
SubMenu: items,
Type: SubmenuType,
}
// Fix up parent pointers
for _, item := range items {
item.parent = result
}
return result
}

View File

@@ -2,6 +2,7 @@ package main
import (
"fmt"
"strconv"
wails "github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/menu"
@@ -10,6 +11,8 @@ import (
// Menu struct
type Menu struct {
runtime *wails.Runtime
dynamicMenuCounter int
}
// WailsInit is called at application startup
@@ -18,21 +21,32 @@ func (m *Menu) WailsInit(runtime *wails.Runtime) error {
m.runtime = runtime
// Setup Menu Listeners
m.runtime.Menu.On("hello", func(m *menu.MenuItem) {
fmt.Printf("The '%s' menu was clicked\n", m.Label)
m.runtime.Menu.On("hello", func(mi *menu.MenuItem) {
fmt.Printf("The '%s' menu was clicked\n", mi.Label)
})
m.runtime.Menu.On("checkbox-menu", func(m *menu.MenuItem) {
fmt.Printf("The '%s' menu was clicked\n", m.Label)
fmt.Printf("It is now %v\n", m.Checked)
// m.Checked = false
// runtime.Menu.Update()
m.runtime.Menu.On("checkbox-menu", func(mi *menu.MenuItem) {
fmt.Printf("The '%s' menu was clicked\n", mi.Label)
fmt.Printf("It is now %v\n", mi.Checked)
})
m.runtime.Menu.On("😀option-1", func(m *menu.MenuItem) {
fmt.Printf("We can use UTF-8 IDs: %s\n", m.Label)
m.runtime.Menu.On("😀option-1", func(mi *menu.MenuItem) {
fmt.Printf("We can use UTF-8 IDs: %s\n", mi.Label)
})
// Setup dynamic menus
m.runtime.Menu.On("Add Menu Item", m.addMenu)
return nil
}
func (m *Menu) addMenu(mi *menu.MenuItem) {
// Get this menu's parent
parent := mi.Parent()
m.dynamicMenuCounter++
menuText := "Dynamic Menu Item " + strconv.Itoa(m.dynamicMenuCounter)
parent.Append(menu.Text(menuText, menuText))
// parent.Append(menu.TextWithAccelerator(menuText, menuText, menu.Accel("[")))
m.runtime.Menu.Update()
}
func createApplicationMenu() *menu.Menu {
// Create menu
@@ -54,6 +68,7 @@ func createApplicationMenu() *menu.Menu {
menu.Front(),
menu.SubMenu("Test Submenu", []*menu.MenuItem{
menu.Text("Plain text", "plain text"),
menu.SubMenu("Accelerators", []*menu.MenuItem{
menu.SubMenu("Modifiers", []*menu.MenuItem{
menu.TextWithAccelerator("Shift accelerator", "Shift", menu.ShiftAccel("o")),
@@ -76,9 +91,6 @@ func createApplicationMenu() *menu.Menu {
menu.TextWithAccelerator("End", "End", menu.Accel("End")),
menu.TextWithAccelerator("Page Up", "Page Up", menu.Accel("Page Up")),
menu.TextWithAccelerator("Page Down", "Page Down", menu.Accel("Page Down")),
menu.TextWithAccelerator("Insert", "Insert", menu.Accel("Insert")),
menu.TextWithAccelerator("PrintScreen", "PrintScreen", menu.Accel("PrintScreen")),
menu.TextWithAccelerator("ScrollLock", "ScrollLock", menu.Accel("ScrollLock")),
menu.TextWithAccelerator("NumLock", "NumLock", menu.Accel("NumLock")),
}),
menu.SubMenu("Function Keys", []*menu.MenuItem{
@@ -102,39 +114,28 @@ func createApplicationMenu() *menu.Menu {
menu.TextWithAccelerator("F18", "F18", menu.Accel("F18")),
menu.TextWithAccelerator("F19", "F19", menu.Accel("F19")),
menu.TextWithAccelerator("F20", "F20", menu.Accel("F20")),
menu.TextWithAccelerator("F21", "F21", menu.Accel("F21")),
menu.TextWithAccelerator("F22", "F22", menu.Accel("F22")),
menu.TextWithAccelerator("F23", "F23", menu.Accel("F23")),
menu.TextWithAccelerator("F24", "F24", menu.Accel("F24")),
menu.TextWithAccelerator("F25", "F25", menu.Accel("F25")),
menu.TextWithAccelerator("F26", "F26", menu.Accel("F26")),
menu.TextWithAccelerator("F27", "F27", menu.Accel("F27")),
menu.TextWithAccelerator("F28", "F28", menu.Accel("F28")),
menu.TextWithAccelerator("F29", "F29", menu.Accel("F29")),
menu.TextWithAccelerator("F30", "F30", menu.Accel("F30")),
menu.TextWithAccelerator("F31", "F31", menu.Accel("F31")),
menu.TextWithAccelerator("F32", "F32", menu.Accel("F32")),
menu.TextWithAccelerator("F33", "F33", menu.Accel("F33")),
menu.TextWithAccelerator("F34", "F34", menu.Accel("F34")),
menu.TextWithAccelerator("F35", "F35", menu.Accel("F35")),
}),
menu.SubMenu("Standard Keys", []*menu.MenuItem{
menu.TextWithAccelerator("Backtick", "Backtick", menu.Accel("`")),
menu.TextWithAccelerator("Plus", "Plus", menu.Accel("+")),
}),
menu.SubMenu("Dynamic Menus", []*menu.MenuItem{
menu.TextWithAccelerator("Add Menu Item", "Add Menu Item", menu.CmdOrCtrlAccel("+")),
menu.Separator(),
}),
}),
&menu.MenuItem{
{
Label: "Disabled Menu",
Type: menu.TextType,
Accelerator: menu.ComboAccel("p", menu.CmdOrCtrl, menu.Shift),
Disabled: true,
},
&menu.MenuItem{
{
Label: "Hidden Menu",
Type: menu.TextType,
Hidden: true,
},
&menu.MenuItem{
{
ID: "checkbox-menu",
Label: "Checkbox Menu",
Type: menu.CheckboxType,