mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-02 19:50:15 +08:00

* Fix WindowSetRGBA API * Change WindowUnFullscreen -> WindowUnfullscreen for consistency RGBA bugfix
579 lines
15 KiB
Go
579 lines
15 KiB
Go
package messagedispatcher
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
|
|
|
"github.com/wailsapp/wails/v2/internal/crypto"
|
|
"github.com/wailsapp/wails/v2/internal/logger"
|
|
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
|
|
"github.com/wailsapp/wails/v2/internal/servicebus"
|
|
)
|
|
|
|
// 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
|
|
|
|
servicebus *servicebus.ServiceBus
|
|
logger logger.CustomLogger
|
|
|
|
// Clients
|
|
clients map[string]*DispatchClient
|
|
lock sync.RWMutex
|
|
|
|
// Context for cancellation
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
|
|
// internal wait group
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
// New dispatcher. Needs a service bus to send to.
|
|
func New(servicebus *servicebus.ServiceBus, logger *logger.Logger) (*Dispatcher, error) {
|
|
// Subscribe to call result messages
|
|
resultChannel, err := servicebus.Subscribe("call:result")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Subscribe to event messages
|
|
eventChannel, err := servicebus.Subscribe("event:emit")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Subscribe to quit messages
|
|
quitChannel, err := servicebus.Subscribe("quit")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Subscribe to window messages
|
|
windowChannel, err := servicebus.Subscribe("window")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Subscribe to dialog events
|
|
dialogChannel, err := servicebus.Subscribe("dialog:select")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
systemChannel, err := servicebus.Subscribe("system:")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
menuChannel, err := servicebus.Subscribe("menufrontend:")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create context
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
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,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// Start the subsystem
|
|
func (d *Dispatcher) Start() error {
|
|
|
|
d.logger.Trace("Starting")
|
|
|
|
d.wg.Add(1)
|
|
|
|
// Spin off a go routine
|
|
go func() {
|
|
defer d.logger.Trace("Shutdown")
|
|
for {
|
|
select {
|
|
case <-d.ctx.Done():
|
|
d.wg.Done()
|
|
return
|
|
case <-d.quitChannel:
|
|
d.processQuit()
|
|
case resultMessage := <-d.resultChannel:
|
|
d.processCallResult(resultMessage)
|
|
case eventMessage := <-d.eventChannel:
|
|
d.processEvent(eventMessage)
|
|
case windowMessage := <-d.windowChannel:
|
|
d.processWindowMessage(windowMessage)
|
|
case dialogMessage := <-d.dialogChannel:
|
|
d.processDialogMessage(dialogMessage)
|
|
case systemMessage := <-d.systemChannel:
|
|
d.processSystemMessage(systemMessage)
|
|
case menuMessage := <-d.menuChannel:
|
|
d.processMenuMessage(menuMessage)
|
|
}
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Dispatcher) processQuit() {
|
|
d.lock.RLock()
|
|
defer d.lock.RUnlock()
|
|
for _, client := range d.clients {
|
|
client.frontend.Quit()
|
|
}
|
|
}
|
|
|
|
// RegisterClient will register the given callback with the dispatcher
|
|
// and return a DispatchClient that the caller can use to send messages
|
|
func (d *Dispatcher) RegisterClient(client Client) *DispatchClient {
|
|
d.lock.Lock()
|
|
defer d.lock.Unlock()
|
|
|
|
// Create ID
|
|
id := d.getUniqueID()
|
|
d.clients[id] = newDispatchClient(id, client, d.logger, d.servicebus)
|
|
|
|
return d.clients[id]
|
|
}
|
|
|
|
// RemoveClient will remove the registered client
|
|
func (d *Dispatcher) RemoveClient(dc *DispatchClient) {
|
|
d.lock.Lock()
|
|
defer d.lock.Unlock()
|
|
delete(d.clients, dc.id)
|
|
}
|
|
|
|
func (d *Dispatcher) getUniqueID() string {
|
|
var uid string
|
|
for {
|
|
uid = crypto.RandomID()
|
|
|
|
if d.clients[uid] == nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
return uid
|
|
}
|
|
|
|
func (d *Dispatcher) processCallResult(result *servicebus.Message) {
|
|
target := result.Target()
|
|
|
|
if target == "" {
|
|
// This is an error. Calls are 1:1!
|
|
d.logger.Fatal("No target for call result: %+v", result)
|
|
}
|
|
|
|
d.lock.RLock()
|
|
client := d.clients[target]
|
|
d.lock.RUnlock()
|
|
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))
|
|
client.frontend.CallResult(result.Data().(string))
|
|
}
|
|
|
|
// processSystem
|
|
func (d *Dispatcher) processSystemMessage(result *servicebus.Message) {
|
|
|
|
d.logger.Trace("Got system in message dispatcher: %+v", result)
|
|
|
|
splitTopic := strings.Split(result.Topic(), ":")
|
|
command := splitTopic[1]
|
|
callbackID := splitTopic[2]
|
|
switch command {
|
|
case "isdarkmode":
|
|
d.lock.RLock()
|
|
for _, client := range d.clients {
|
|
client.frontend.DarkModeEnabled(callbackID)
|
|
break
|
|
}
|
|
d.lock.RUnlock()
|
|
|
|
default:
|
|
d.logger.Error("Unknown system command: %s", command)
|
|
}
|
|
}
|
|
|
|
// processEvent will
|
|
func (d *Dispatcher) processEvent(result *servicebus.Message) {
|
|
|
|
d.logger.Trace("Got event in message dispatcher: %+v", result)
|
|
|
|
splitTopic := strings.Split(result.Topic(), ":")
|
|
eventType := splitTopic[1]
|
|
switch eventType {
|
|
case "emit":
|
|
eventFrom := splitTopic[3]
|
|
if eventFrom == "g" {
|
|
// This was sent from Go - notify frontend
|
|
eventData := result.Data().(*message.EventMessage)
|
|
// Unpack event
|
|
payload, err := json.Marshal(eventData)
|
|
if err != nil {
|
|
d.logger.Error("Unable to marshal eventData: %s", err.Error())
|
|
return
|
|
}
|
|
d.lock.RLock()
|
|
for _, client := range d.clients {
|
|
client.frontend.NotifyEvent(string(payload))
|
|
}
|
|
d.lock.RUnlock()
|
|
}
|
|
default:
|
|
d.logger.Error("Unknown event type: %s", eventType)
|
|
}
|
|
}
|
|
|
|
// processWindowMessage processes messages intended for the window
|
|
func (d *Dispatcher) processWindowMessage(result *servicebus.Message) {
|
|
d.lock.RLock()
|
|
defer d.lock.RUnlock()
|
|
splitTopic := strings.Split(result.Topic(), ":")
|
|
command := splitTopic[1]
|
|
switch command {
|
|
case "settitle":
|
|
title, ok := result.Data().(string)
|
|
if !ok {
|
|
d.logger.Error("Invalid title for 'window:settitle' : %#v", result.Data())
|
|
return
|
|
}
|
|
// Notify clients
|
|
for _, client := range d.clients {
|
|
client.frontend.WindowSetTitle(title)
|
|
}
|
|
case "fullscreen":
|
|
// Notify clients
|
|
for _, client := range d.clients {
|
|
client.frontend.WindowFullscreen()
|
|
}
|
|
case "unfullscreen":
|
|
// Notify clients
|
|
for _, client := range d.clients {
|
|
client.frontend.WindowUnfullscreen()
|
|
}
|
|
case "setcolour":
|
|
colour, ok := result.Data().(int)
|
|
if !ok {
|
|
d.logger.Error("Invalid colour for 'window:setcolour' : %#v", result.Data())
|
|
return
|
|
}
|
|
// Notify clients
|
|
for _, client := range d.clients {
|
|
client.frontend.WindowSetColour(colour)
|
|
}
|
|
case "show":
|
|
// Notify clients
|
|
for _, client := range d.clients {
|
|
client.frontend.WindowShow()
|
|
}
|
|
case "hide":
|
|
// Notify clients
|
|
for _, client := range d.clients {
|
|
client.frontend.WindowHide()
|
|
}
|
|
case "center":
|
|
// Notify clients
|
|
for _, client := range d.clients {
|
|
client.frontend.WindowCenter()
|
|
}
|
|
case "maximise":
|
|
// Notify clients
|
|
for _, client := range d.clients {
|
|
client.frontend.WindowMaximise()
|
|
}
|
|
case "unmaximise":
|
|
// Notify clients
|
|
for _, client := range d.clients {
|
|
client.frontend.WindowUnmaximise()
|
|
}
|
|
case "minimise":
|
|
// Notify clients
|
|
for _, client := range d.clients {
|
|
client.frontend.WindowMinimise()
|
|
}
|
|
case "unminimise":
|
|
// Notify clients
|
|
for _, client := range d.clients {
|
|
client.frontend.WindowUnminimise()
|
|
}
|
|
case "position":
|
|
// We need 2 arguments
|
|
if len(splitTopic) != 4 {
|
|
d.logger.Error("Invalid number of parameters for 'window:position' : %#v", result.Data())
|
|
return
|
|
}
|
|
x, err1 := strconv.Atoi(splitTopic[2])
|
|
y, err2 := strconv.Atoi(splitTopic[3])
|
|
if err1 != nil || err2 != nil {
|
|
d.logger.Error("Invalid integer parameters for 'window:position' : %#v", result.Data())
|
|
return
|
|
}
|
|
// Notify clients
|
|
for _, client := range d.clients {
|
|
client.frontend.WindowPosition(x, y)
|
|
}
|
|
case "size":
|
|
// We need 2 arguments
|
|
if len(splitTopic) != 4 {
|
|
d.logger.Error("Invalid number of parameters for 'window:size' : %#v", result.Data())
|
|
return
|
|
}
|
|
w, err1 := strconv.Atoi(splitTopic[2])
|
|
h, err2 := strconv.Atoi(splitTopic[3])
|
|
if err1 != nil || err2 != nil {
|
|
d.logger.Error("Invalid integer parameters for 'window:size' : %#v", result.Data())
|
|
return
|
|
}
|
|
// Notifh clients
|
|
for _, client := range d.clients {
|
|
client.frontend.WindowSize(w, h)
|
|
}
|
|
case "minsize":
|
|
// We need 2 arguments
|
|
if len(splitTopic) != 4 {
|
|
d.logger.Error("Invalid number of parameters for 'window:minsize' : %#v", result.Data())
|
|
return
|
|
}
|
|
w, err1 := strconv.Atoi(splitTopic[2])
|
|
h, err2 := strconv.Atoi(splitTopic[3])
|
|
if err1 != nil || err2 != nil {
|
|
d.logger.Error("Invalid integer parameters for 'window:minsize' : %#v", result.Data())
|
|
return
|
|
}
|
|
// Notifh clients
|
|
for _, client := range d.clients {
|
|
client.frontend.WindowSetMinSize(w, h)
|
|
}
|
|
case "maxsize":
|
|
// We need 2 arguments
|
|
if len(splitTopic) != 4 {
|
|
d.logger.Error("Invalid number of parameters for 'window:maxsize' : %#v", result.Data())
|
|
return
|
|
}
|
|
w, err1 := strconv.Atoi(splitTopic[2])
|
|
h, err2 := strconv.Atoi(splitTopic[3])
|
|
if err1 != nil || err2 != nil {
|
|
d.logger.Error("Invalid integer parameters for 'window:maxsize' : %#v", result.Data())
|
|
return
|
|
}
|
|
// Notifh clients
|
|
for _, client := range d.clients {
|
|
client.frontend.WindowSetMaxSize(w, h)
|
|
}
|
|
default:
|
|
d.logger.Error("Unknown window command: %s", command)
|
|
}
|
|
d.logger.Trace("Got window in message dispatcher: %+v", result)
|
|
|
|
}
|
|
|
|
// processDialogMessage processes dialog messages
|
|
func (d *Dispatcher) processDialogMessage(result *servicebus.Message) {
|
|
splitTopic := strings.Split(result.Topic(), ":")
|
|
if len(splitTopic) < 4 {
|
|
d.logger.Error("Invalid dialog message : %#v", result.Data())
|
|
return
|
|
}
|
|
|
|
command := splitTopic[1]
|
|
switch command {
|
|
case "select":
|
|
dialogType := splitTopic[2]
|
|
switch dialogType {
|
|
case "open":
|
|
dialogOptions, ok := result.Data().(runtime.OpenDialogOptions)
|
|
if !ok {
|
|
d.logger.Error("Invalid data for 'dialog:select:open' : %#v", result.Data())
|
|
return
|
|
}
|
|
// This is hardcoded in the sender too
|
|
callbackID := splitTopic[3]
|
|
|
|
// 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.OpenFileDialog(dialogOptions, callbackID)
|
|
}
|
|
case "openmultiple":
|
|
dialogOptions, ok := result.Data().(runtime.OpenDialogOptions)
|
|
if !ok {
|
|
d.logger.Error("Invalid data for 'dialog:select:openmultiple' : %#v", result.Data())
|
|
return
|
|
}
|
|
// This is hardcoded in the sender too
|
|
callbackID := splitTopic[3]
|
|
|
|
// 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.OpenMultipleFilesDialog(dialogOptions, callbackID)
|
|
}
|
|
case "directory":
|
|
dialogOptions, ok := result.Data().(runtime.OpenDialogOptions)
|
|
if !ok {
|
|
d.logger.Error("Invalid data for 'dialog:select:directory' : %#v", result.Data())
|
|
return
|
|
}
|
|
// This is hardcoded in the sender too
|
|
callbackID := splitTopic[3]
|
|
|
|
// 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.OpenDirectoryDialog(dialogOptions, callbackID)
|
|
}
|
|
case "save":
|
|
dialogOptions, ok := result.Data().(runtime.SaveDialogOptions)
|
|
if !ok {
|
|
d.logger.Error("Invalid data for 'dialog:select:save' : %#v", result.Data())
|
|
return
|
|
}
|
|
// This is hardcoded in the sender too
|
|
callbackID := splitTopic[3]
|
|
|
|
// 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.SaveDialog(dialogOptions, callbackID)
|
|
}
|
|
case "message":
|
|
dialogOptions, ok := result.Data().(runtime.MessageDialogOptions)
|
|
if !ok {
|
|
d.logger.Error("Invalid data for 'dialog:select:message' : %#v", result.Data())
|
|
return
|
|
}
|
|
// This is hardcoded in the sender too
|
|
callbackID := splitTopic[3]
|
|
|
|
// 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.MessageDialog(dialogOptions, callbackID)
|
|
}
|
|
default:
|
|
d.logger.Error("Unknown dialog type: %s", dialogType)
|
|
}
|
|
|
|
default:
|
|
d.logger.Error("Unknown dialog command: %s", command)
|
|
}
|
|
|
|
}
|
|
|
|
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 "updateappmenu":
|
|
|
|
updatedMenu, ok := result.Data().(string)
|
|
if !ok {
|
|
d.logger.Error("Invalid data for 'menufrontend:updateappmenu' : %#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.SetApplicationMenu(updatedMenu)
|
|
}
|
|
|
|
case "settraymenu":
|
|
trayMenuJSON, ok := result.Data().(string)
|
|
if !ok {
|
|
d.logger.Error("Invalid data for 'menufrontend:settraymenu' : %#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.SetTrayMenu(trayMenuJSON)
|
|
}
|
|
|
|
case "updatecontextmenu":
|
|
updatedContextMenu, ok := result.Data().(string)
|
|
if !ok {
|
|
d.logger.Error("Invalid data for 'menufrontend:updatecontextmenu' : %#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.UpdateContextMenu(updatedContextMenu)
|
|
}
|
|
|
|
case "updatetraymenulabel":
|
|
updatedTrayMenuLabel, ok := result.Data().(string)
|
|
if !ok {
|
|
d.logger.Error("Invalid data for 'menufrontend:updatetraymenulabel' : %#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.UpdateTrayMenuLabel(updatedTrayMenuLabel)
|
|
}
|
|
case "deletetraymenu":
|
|
traymenuid, ok := result.Data().(string)
|
|
if !ok {
|
|
d.logger.Error("Invalid data for 'menufrontend:updatetraymenulabel' : %#v",
|
|
result.Data())
|
|
return
|
|
}
|
|
|
|
for _, client := range d.clients {
|
|
client.frontend.DeleteTrayMenuByID(traymenuid)
|
|
}
|
|
|
|
default:
|
|
d.logger.Error("Unknown menufrontend command: %s", command)
|
|
}
|
|
}
|
|
|
|
func (d *Dispatcher) Close() {
|
|
d.cancel()
|
|
d.wg.Wait()
|
|
}
|