Support context menu data

Support StartHidden
This commit is contained in:
Lea Anthony
2020-12-18 15:50:25 +11:00
parent 34ac62e4ac
commit a8995c5377
23 changed files with 773 additions and 247 deletions
+2 -3
View File
@@ -25,7 +25,7 @@ type App struct {
}
// CreateApp returns a null application
func CreateApp(options *options.App) *App {
func CreateApp(_ *options.App) *App {
return &App{}
}
@@ -37,6 +37,5 @@ func (a *App) Run() error {
}
// Bind the dummy interface
func (a *App) Bind(dummy interface{}) error {
return nil
func (a *App) Bind(_ interface{}) {
}
+24 -9
View File
@@ -23,14 +23,15 @@ type App struct {
options *options.App
// Subsystems
log *subsystem.Log
runtime *subsystem.Runtime
event *subsystem.Event
binding *subsystem.Binding
call *subsystem.Call
menu *subsystem.Menu
tray *subsystem.Tray
dispatcher *messagedispatcher.Dispatcher
log *subsystem.Log
runtime *subsystem.Runtime
event *subsystem.Event
binding *subsystem.Binding
call *subsystem.Call
menu *subsystem.Menu
tray *subsystem.Tray
contextmenus *subsystem.ContextMenus
dispatcher *messagedispatcher.Dispatcher
// Indicates if the app is in debug mode
debug bool
@@ -94,8 +95,9 @@ func (a *App) Run() error {
// Start the runtime
applicationMenu := options.GetApplicationMenu(a.options)
trayMenu := options.GetTrayMenu(a.options)
contextMenus := options.GetContextMenus(a.options)
runtimesubsystem, err := subsystem.NewRuntime(a.servicebus, a.logger, applicationMenu, trayMenu)
runtimesubsystem, err := subsystem.NewRuntime(a.servicebus, a.logger, applicationMenu, trayMenu, contextMenus)
if err != nil {
return err
}
@@ -179,6 +181,19 @@ func (a *App) Run() error {
}
}
// Optionally start the context menu subsystem
if contextMenus != nil {
contextmenussubsystem, err := subsystem.NewContextMenus(contextMenus, a.servicebus, a.logger)
if err != nil {
return err
}
a.contextmenus = contextmenussubsystem
err = a.contextmenus.Start()
if err != nil {
return err
}
}
// Start the call subsystem
call, err := subsystem.NewCall(a.servicebus, a.logger, a.bindings.DB(), a.runtime.GoRuntime())
if err != nil {
+2
View File
@@ -34,4 +34,6 @@ extern void SaveDialog(void *appPointer, char *callbackID, char *title, char *fi
extern void DarkModeEnabled(void *appPointer, char *callbackID);
extern void UpdateMenu(void *app, char *menuAsJSON);
extern void UpdateTray(void *app, char *menuAsJSON);
extern void UpdateContextMenus(void *app, char *contextMenusAsJSON);
#endif
+17
View File
@@ -13,6 +13,7 @@ import "C"
import (
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
"strconv"
@@ -188,3 +189,19 @@ func (c *Client) UpdateTray(menu *menu.Menu) {
}
C.UpdateTray(c.app.app, c.app.string2CString(string(trayMenuJSON)))
}
func (c *Client) UpdateContextMenus(contextMenus *menu.ContextMenus) {
// Guard against nil contextMenus
if contextMenus == nil {
return
}
// Process the menu
contextMenusJSON, err := json.Marshal(contextMenus)
fmt.Printf("\n\nUPDATED CONTEXT MENUS:\n %+v\n\n", string(contextMenusJSON))
if err != nil {
c.app.logger.Error("Error processing updated Context Menus: %s", err.Error())
return
}
C.UpdateContextMenus(c.app.app, c.app.string2CString(string(contextMenusJSON)))
}
File diff suppressed because it is too large Load Diff
@@ -21,6 +21,7 @@ extern void SetContextMenus(void *, const char *);
import "C"
import (
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v2/pkg/options"
)
@@ -116,6 +117,7 @@ func (a *Application) processPlatformSettings() error {
contextMenus := options.GetContextMenus(a.config)
if contextMenus != nil {
contextMenusJSON, err := json.Marshal(contextMenus)
fmt.Printf("\n\nCONTEXT MENUS:\n %+v\n\n", string(contextMenusJSON))
if err != nil {
return err
}
@@ -33,6 +33,7 @@ type Client interface {
DarkModeEnabled(callbackID string)
UpdateMenu(menu *menu.Menu)
UpdateTray(menu *menu.Menu)
UpdateContextMenus(contextMenus *menu.ContextMenus)
}
// DispatchClient is what the frontends use to interface with the
@@ -0,0 +1,43 @@
package message
import (
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
)
// ContextMenusOnMessage is used to emit listener registration requests
// on the service bus
type ContextMenusOnMessage 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, string)
}
// contextMenusMessageParser does what it says on the tin!
func contextMenusMessageParser(message string) (*parsedMessage, error) {
// Sanity check: Menu messages must be at least 2 bytes
if len(message) < 3 {
return nil, fmt.Errorf("context menus message was an invalid length")
}
var topic string
var data interface{}
// Switch the message type
switch message[1] {
case 'C':
contextMenuData := message[2:]
topic = "contextmenus:clicked"
data = contextMenuData
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
}
@@ -20,6 +20,7 @@ var messageParsers = map[byte]func(string) (*parsedMessage, error){
'S': systemMessageParser,
'M': menuMessageParser,
'T': trayMessageParser,
'X': contextMenusMessageParser,
}
// Parse will attempt to parse the given message
@@ -20,7 +20,7 @@ func trayMessageParser(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")
return nil, fmt.Errorf("tray message was an invalid length")
}
var topic string
@@ -17,15 +17,16 @@ import (
// Dispatcher translates messages received from the frontend
// and publishes them onto the service bus
type Dispatcher struct {
quitChannel <-chan *servicebus.Message
resultChannel <-chan *servicebus.Message
eventChannel <-chan *servicebus.Message
windowChannel <-chan *servicebus.Message
dialogChannel <-chan *servicebus.Message
systemChannel <-chan *servicebus.Message
menuChannel <-chan *servicebus.Message
trayChannel <-chan *servicebus.Message
running bool
quitChannel <-chan *servicebus.Message
resultChannel <-chan *servicebus.Message
eventChannel <-chan *servicebus.Message
windowChannel <-chan *servicebus.Message
dialogChannel <-chan *servicebus.Message
systemChannel <-chan *servicebus.Message
menuChannel <-chan *servicebus.Message
contextMenuChannel <-chan *servicebus.Message
trayChannel <-chan *servicebus.Message
running bool
servicebus *servicebus.ServiceBus
logger logger.CustomLogger
@@ -77,23 +78,29 @@ func New(servicebus *servicebus.ServiceBus, logger *logger.Logger) (*Dispatcher,
return nil, err
}
contextMenuChannel, err := servicebus.Subscribe("contextmenufrontend:")
if err != nil {
return nil, err
}
trayChannel, err := servicebus.Subscribe("trayfrontend:")
if err != nil {
return nil, err
}
result := &Dispatcher{
servicebus: servicebus,
eventChannel: eventChannel,
logger: logger.CustomLogger("Message Dispatcher"),
clients: make(map[string]*DispatchClient),
resultChannel: resultChannel,
quitChannel: quitChannel,
windowChannel: windowChannel,
dialogChannel: dialogChannel,
systemChannel: systemChannel,
menuChannel: menuChannel,
trayChannel: trayChannel,
servicebus: servicebus,
eventChannel: eventChannel,
logger: logger.CustomLogger("Message Dispatcher"),
clients: make(map[string]*DispatchClient),
resultChannel: resultChannel,
quitChannel: quitChannel,
windowChannel: windowChannel,
dialogChannel: dialogChannel,
systemChannel: systemChannel,
menuChannel: menuChannel,
trayChannel: trayChannel,
contextMenuChannel: contextMenuChannel,
}
return result, nil
@@ -125,6 +132,8 @@ func (d *Dispatcher) Start() error {
d.processSystemMessage(systemMessage)
case menuMessage := <-d.menuChannel:
d.processMenuMessage(menuMessage)
case contextMenuMessage := <-d.contextMenuChannel:
d.processContextMenuMessage(contextMenuMessage)
case trayMessage := <-d.trayChannel:
d.processTrayMessage(trayMessage)
}
@@ -446,6 +455,34 @@ func (d *Dispatcher) processMenuMessage(result *servicebus.Message) {
d.logger.Error("Unknown menufrontend command: %s", command)
}
}
func (d *Dispatcher) processContextMenuMessage(result *servicebus.Message) {
splitTopic := strings.Split(result.Topic(), ":")
if len(splitTopic) < 2 {
d.logger.Error("Invalid contextmenu message : %#v", result.Data())
return
}
command := splitTopic[1]
switch command {
case "update":
updatedContextMenus, ok := result.Data().(*menu.ContextMenus)
if !ok {
d.logger.Error("Invalid data for 'contextmenufrontend:update' : %#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.UpdateContextMenus(updatedContextMenus)
}
default:
d.logger.Error("Unknown contextmenufrontend command: %s", command)
}
}
func (d *Dispatcher) processTrayMessage(result *servicebus.Message) {
splitTopic := strings.Split(result.Topic(), ":")
+48
View File
@@ -0,0 +1,48 @@
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"
)
// ContextMenus defines all ContextMenu related operations
type ContextMenus interface {
On(menuID string, callback func(*menu.MenuItem, string))
Update()
GetByID(menuID string) *menu.MenuItem
RemoveByID(id string) bool
}
type contextMenus struct {
bus *servicebus.ServiceBus
contextmenus *menu.ContextMenus
}
// newContextMenus creates a new ContextMenu struct
func newContextMenus(bus *servicebus.ServiceBus, contextmenus *menu.ContextMenus) ContextMenus {
return &contextMenus{
bus: bus,
contextmenus: contextmenus,
}
}
// On registers a listener for a particular event
func (t *contextMenus) On(menuID string, callback func(*menu.MenuItem, string)) {
t.bus.Publish("contextmenus:on", &message.ContextMenusOnMessage{
MenuID: menuID,
Callback: callback,
})
}
func (t *contextMenus) Update() {
t.bus.Publish("contextmenus:update", t.contextmenus)
}
func (t *contextMenus) GetByID(menuItemID string) *menu.MenuItem {
return t.contextmenus.GetByID(menuItemID)
}
func (t *contextMenus) RemoveByID(menuItemID string) bool {
return t.contextmenus.RemoveByID(menuItemID)
}
+2 -2
View File
@@ -57,10 +57,10 @@ export function Init() {
e.preventDefault();
}
if( contextMenuId != null ) {
let contextData = currentElement.dataset['wails-context-menu-data'];
let message = {
id: contextMenuId,
x: e.clientX,
y: e.clientY,
data: contextData || "",
};
window.webkit.messageHandlers.contextMenu.postMessage(JSON.stringify(message));
}
+22 -20
View File
@@ -7,30 +7,32 @@ import (
// Runtime is a means for the user to interact with the application at runtime
type Runtime struct {
Browser Browser
Events Events
Window Window
Dialog Dialog
System System
Menu Menu
Tray Tray
Store *StoreProvider
Log Log
bus *servicebus.ServiceBus
Browser Browser
Events Events
Window Window
Dialog Dialog
System System
Menu Menu
ContextMenu ContextMenus
Tray Tray
Store *StoreProvider
Log Log
bus *servicebus.ServiceBus
}
// New creates a new runtime
func New(serviceBus *servicebus.ServiceBus, menu *menu.Menu, trayMenu *menu.Menu) *Runtime {
func New(serviceBus *servicebus.ServiceBus, menu *menu.Menu, trayMenu *menu.Menu, contextMenus *menu.ContextMenus) *Runtime {
result := &Runtime{
Browser: newBrowser(),
Events: newEvents(serviceBus),
Window: newWindow(serviceBus),
Dialog: newDialog(serviceBus),
System: newSystem(serviceBus),
Menu: newMenu(serviceBus, menu),
Tray: newTray(serviceBus, trayMenu),
Log: newLog(serviceBus),
bus: serviceBus,
Browser: newBrowser(),
Events: newEvents(serviceBus),
Window: newWindow(serviceBus),
Dialog: newDialog(serviceBus),
System: newSystem(serviceBus),
Menu: newMenu(serviceBus, menu),
Tray: newTray(serviceBus, trayMenu),
ContextMenu: newContextMenus(serviceBus, contextMenus),
Log: newLog(serviceBus),
bus: serviceBus,
}
result.Store = newStore(result)
return result
+1 -1
View File
@@ -20,7 +20,7 @@ type trayRuntime struct {
}
// newTray creates a new Menu struct
func newTray(bus *servicebus.ServiceBus, menu *menu.Menu) Menu {
func newTray(bus *servicebus.ServiceBus, menu *menu.Menu) Tray {
return &trayRuntime{
bus: bus,
trayMenu: menu,
+200
View File
@@ -0,0 +1,200 @@
package subsystem
import (
"encoding/json"
"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"
)
// ContextMenus is the subsystem that handles the operation of context menus. It manages all service bus messages
// starting with "contextmenus".
type ContextMenus struct {
quitChannel <-chan *servicebus.Message
menuChannel <-chan *servicebus.Message
running bool
// Event listeners
listeners map[string][]func(*menu.MenuItem, string)
menuItems map[string]*menu.MenuItem
notifyLock sync.RWMutex
// logger
logger logger.CustomLogger
// The context menus
contextMenus *menu.ContextMenus
// Service Bus
bus *servicebus.ServiceBus
}
// NewContextMenus creates a new context menu subsystem
func NewContextMenus(contextMenus *menu.ContextMenus, bus *servicebus.ServiceBus, logger *logger.Logger) (*ContextMenus, error) {
// Register quit channel
quitChannel, err := bus.Subscribe("quit")
if err != nil {
return nil, err
}
// Subscribe to menu messages
menuChannel, err := bus.Subscribe("contextmenus:")
if err != nil {
return nil, err
}
result := &ContextMenus{
quitChannel: quitChannel,
menuChannel: menuChannel,
logger: logger.CustomLogger("Context Menu Subsystem"),
listeners: make(map[string][]func(*menu.MenuItem, string)),
menuItems: make(map[string]*menu.MenuItem),
contextMenus: contextMenus,
bus: bus,
}
// Build up list of item/id pairs
result.processContextMenus(contextMenus)
return result, nil
}
type contextMenuData struct {
MenuItemID string `json:"menuItemID"`
Data string `json:"data"`
}
// Start the subsystem
func (c *ContextMenus) Start() error {
c.logger.Trace("Starting")
c.running = true
// Spin off a go routine
go func() {
for c.running {
select {
case <-c.quitChannel:
c.running = false
break
case menuMessage := <-c.menuChannel:
splitTopic := strings.Split(menuMessage.Topic(), ":")
menuMessageType := splitTopic[1]
switch menuMessageType {
case "clicked":
if len(splitTopic) != 2 {
c.logger.Error("Received clicked message with invalid topic format. Expected 2 sections in topic, got %s", splitTopic)
continue
}
c.logger.Trace("Got Context Menu clicked Message: %s %+v", menuMessage.Topic(), menuMessage.Data())
contextMenuDataJSON := menuMessage.Data().(string)
var data contextMenuData
err := json.Unmarshal([]byte(contextMenuDataJSON), &data)
if err != nil {
c.logger.Trace("Cannot process contextMenuDataJSON %s", string(contextMenuDataJSON))
return
}
// Get the menu item
menuItem := c.menuItems[data.MenuItemID]
if menuItem == nil {
c.logger.Trace("Cannot process menuitem id %s - unknown", data.MenuItemID)
return
}
// Is the menu item a checkbox?
if menuItem.Type == menu.CheckboxType {
// Toggle state
menuItem.Checked = !menuItem.Checked
}
// Notify listeners
c.notifyListeners(data, menuItem)
case "on":
listenerDetails := menuMessage.Data().(*message.ContextMenusOnMessage)
id := listenerDetails.MenuID
c.listeners[id] = append(c.listeners[id], listenerDetails.Callback)
// Make sure we catch any menu updates
case "update":
updatedMenu := menuMessage.Data().(*menu.ContextMenus)
c.processContextMenus(updatedMenu)
// Notify frontend of menu change
c.bus.Publish("contextmenufrontend:update", updatedMenu)
default:
c.logger.Error("unknown context menu message: %+v", menuMessage)
}
}
}
// Call shutdown
c.shutdown()
}()
return nil
}
func (c *ContextMenus) processContextMenus(contextMenus *menu.ContextMenus) {
// Initialise the variables
c.menuItems = make(map[string]*menu.MenuItem)
c.contextMenus = contextMenus
for _, contextMenu := range contextMenus.Items {
for _, item := range contextMenu.Items {
c.processMenuItem(item)
}
}
}
func (c *ContextMenus) processMenuItem(item *menu.MenuItem) {
if item.SubMenu != nil {
for _, submenuitem := range item.SubMenu {
c.processMenuItem(submenuitem)
}
return
}
if item.ID != "" {
if c.menuItems[item.ID] != nil {
c.logger.Error("Context Menu id '%s' is used by multiple menu items: %s %s", c.menuItems[item.ID].Label, item.Label)
return
}
c.menuItems[item.ID] = item
}
}
// Notifies listeners that the given menu was clicked
func (c *ContextMenus) notifyListeners(contextData contextMenuData, menuItem *menu.MenuItem) {
// Get list of menu listeners
listeners := c.listeners[contextData.MenuItemID]
if listeners == nil {
c.logger.Trace("No listeners for MenuItem with ID '%s'", contextData.MenuItemID)
return
}
// Lock the listeners
c.notifyLock.Lock()
// Callback in goroutine
for _, listener := range listeners {
go listener(menuItem, contextData.Data)
}
// Unlock
c.notifyLock.Unlock()
}
func (c *ContextMenus) shutdown() {
c.logger.Trace("Shutdown")
}
+2 -2
View File
@@ -24,7 +24,7 @@ type Runtime struct {
}
// NewRuntime creates a new runtime subsystem
func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger, menu *menu.Menu, trayMenu *menu.Menu) (*Runtime, error) {
func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger, menu *menu.Menu, trayMenu *menu.Menu, contextMenus *menu.ContextMenus) (*Runtime, error) {
// Register quit channel
quitChannel, err := bus.Subscribe("quit")
@@ -42,7 +42,7 @@ func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger, menu *menu.Me
quitChannel: quitChannel,
runtimeChannel: runtimeChannel,
logger: logger.CustomLogger("Runtime Subsystem"),
runtime: runtime.New(bus, menu, trayMenu),
runtime: runtime.New(bus, menu, trayMenu, contextMenus),
}
return result, nil
+38
View File
@@ -0,0 +1,38 @@
package menu
type ContextMenus struct {
Items map[string]*Menu
}
func NewContextMenus() *ContextMenus {
return &ContextMenus{
Items: make(map[string]*Menu),
}
}
func (c *ContextMenus) AddMenu(ID string, menu *Menu) {
c.Items[ID] = menu
}
func (c *ContextMenus) GetByID(menuID string) *MenuItem {
// Loop over menu items
for _, item := range c.Items {
result := item.GetByID(menuID)
if result != nil {
return result
}
}
return nil
}
func (c *ContextMenus) RemoveByID(id string) bool {
// Loop over menu items
for _, item := range c.Items {
result := item.RemoveByID(id)
if result == true {
return result
}
}
return false
}
+1 -1
View File
@@ -10,5 +10,5 @@ type Options struct {
WindowBackgroundIsTranslucent bool
Menu *menu.Menu
Tray *menu.Menu
ContextMenus map[string]*menu.Menu
ContextMenus *menu.ContextMenus
}
+8 -6
View File
@@ -24,7 +24,7 @@ type App struct {
StartHidden bool
DevTools bool
RGBA int
ContextMenus map[string]*menu.Menu
ContextMenus *menu.ContextMenus
Tray *menu.Menu
Menu *menu.Menu
Mac *mac.Options
@@ -97,11 +97,11 @@ func GetApplicationMenu(appoptions *App) *menu.Menu {
return result
}
func GetContextMenus(appoptions *App) map[string]*menu.Menu {
var result map[string]*menu.Menu
func GetContextMenus(appoptions *App) *menu.ContextMenus {
var result *menu.ContextMenus
result = appoptions.ContextMenus
var contextMenuOverrides map[string]*menu.Menu
var contextMenuOverrides *menu.ContextMenus
switch runtime.GOOS {
case "darwin":
if appoptions.Mac != nil {
@@ -118,8 +118,10 @@ func GetContextMenus(appoptions *App) map[string]*menu.Menu {
}
// Overwrite defaults with OS Specific context menus
for id, contextMenu := range contextMenuOverrides {
result[id] = contextMenu
if contextMenuOverrides != nil {
for id, contextMenu := range contextMenuOverrides.Items {
result.AddMenu(id, contextMenu)
}
}
return result

Some files were not shown because too many files have changed in this diff Show More