Skip to content

Commit

Permalink
Refactored remote messaging
Browse files Browse the repository at this point in the history
  • Loading branch information
asticode committed Dec 3, 2017
1 parent 692760f commit d29e02b
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 77 deletions.
149 changes: 84 additions & 65 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,43 +117,72 @@ w.Maximize()

Check out the [Window doc](https://godoc.org/github.com/asticode/go-astilectron#Window) for a list of all exported methods

## Send messages between GO and your webserver
## Send messages from GO to Javascript

### Javascript

```javascript
// This will wait for the astilectron namespace to be ready
document.addEventListener('astilectron-ready', function() {
// This will listen to messages sent by GO
astilectron.onMessage(function(message) {
// Process message
if (message === "hello") {
return "world";
}
});
})
```

In your webserver add the following javascript to any of the pages you want to interact with:
### GO

```html
<script>
// This will wait for the astilectron namespace to be ready
document.addEventListener('astilectron-ready', function() {
// This will listen to messages sent by GO
astilectron.listen(function(message) {
// This will send a message back to GO
astilectron.send("I'm good bro")
});
})
</script>
```go
// This will send a message and execute a callback
// Callbacks are optional
w.SendMessage("hello", func(e Event) {
// Unmarshal
var s string
e.Message.Unmarshal(&s)

// Process message
astilog.Debugf("received %s", s)
})
```

In your GO app add the following:

This will print `received world` in the GO output

## Send messages from Javascript to GO

### GO

```go
// Listen to messages sent by webserver
w.On(astilectron.EventNameWindowEventMessage, func(e astilectron.Event) (deleteListener bool) {
var m string
e.Message.Unmarshal(&m)
astilog.Infof("Received message %s", m)
return
// This will listen to messages sent by Javascript
w.OnMessage(func(e Event) interface{} {
// Unmarshal
var s string
e.Message.Unmarshal(&s)

// Process message
if *e.Message == "hello" {
return "world"
}
return nil
})
```

### Javascript

// Send message to webserver
w.Send("What's up?")
```javascript
// This will wait for the astilectron namespace to be ready
document.addEventListener('astilectron-ready', function() {
// This will send a message to GO
astilectron.sendMessage("hello", function(message) {
console.log("received " + message)
});
})
```

And that's it!

NOTE: needless to say that the message can be something other than a string. A custom struct for instance!
This will print "received world" in the Javascript output

## Play with the window's session

Expand Down Expand Up @@ -316,66 +345,56 @@ t.Create()

## Dialogs

In your webserver add one of the following javascript to achieve any kind of dialog.

### Error box

```html
<script>
// This will wait for the astilectron namespace to be ready
document.addEventListener('astilectron-ready', function() {
// This will open the dialog
astilectron.showErrorBox("My Title", "My content")
})
</script>
```javascript
// This will wait for the astilectron namespace to be ready
document.addEventListener('astilectron-ready', function() {
// This will open the dialog
astilectron.showErrorBox("My Title", "My content")
})
```

### Message box

```html
<script>
// This will wait for the astilectron namespace to be ready
document.addEventListener('astilectron-ready', function() {
// This will open the dialog
astilectron.showMessageBox({message: "My message", title: "My Title"})
})
</script>
```javascript
// This will wait for the astilectron namespace to be ready
document.addEventListener('astilectron-ready', function() {
// This will open the dialog
astilectron.showMessageBox({message: "My message", title: "My Title"})
})
```

### Open dialog

```html
<script>
// This will wait for the astilectron namespace to be ready
document.addEventListener('astilectron-ready', function() {
// This will open the dialog
astilectron.showOpenDialog({properties: ['openFile', 'multiSelections'], title: "My Title"}, function(paths) {
console.log("chosen paths are ", paths)
})
```javascript
// This will wait for the astilectron namespace to be ready
document.addEventListener('astilectron-ready', function() {
// This will open the dialog
astilectron.showOpenDialog({properties: ['openFile', 'multiSelections'], title: "My Title"}, function(paths) {
console.log("chosen paths are ", paths)
})
</script>
})
```

### Save dialog

```html
<script>
// This will wait for the astilectron namespace to be ready
document.addEventListener('astilectron-ready', function() {
// This will open the dialog
astilectron.showSaveDialog({title: "My title"}, function(filename) {
console.log("chosen filename is ", filename)
})
```javascript
// This will wait for the astilectron namespace to be ready
document.addEventListener('astilectron-ready', function() {
// This will open the dialog
astilectron.showSaveDialog({title: "My title"}, function(filename) {
console.log("chosen filename is ", filename)
})
</script>
})
```

# Features and roadmap

- [x] custom branding (custom app name, app icon, etc.)
- [x] window basic methods (create, show, close, resize, minimize, maximize, ...)
- [x] window basic events (close, blur, focus, unresponsive, crashed, ...)
- [x] remote messaging (messages between GO and the JS in the webserver)
- [x] remote messaging (messages between GO and Javascript)
- [x] single binary distribution
- [x] multi screens/displays
- [x] menu methods and events (create, insert, append, popup, clicked, ...)
Expand Down
2 changes: 1 addition & 1 deletion astilectron.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (

// Versions
const (
VersionAstilectron = "0.13.0"
VersionAstilectron = "0.14.0"
VersionElectron = "1.8.1"
)

Expand Down
1 change: 1 addition & 0 deletions event.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Event struct {
// This is a list of all possible payloads.
// A choice was made not to use interfaces since it's a pain in the ass asserting each an every payload afterwards
// We use pointers so that omitempty works
CallbackID string `json:"callbackId,omitempty"`
Displays *EventDisplays `json:"displays,omitempty"`
Menu *EventMenu `json:"menu,omitempty"`
MenuItem *EventMenuItem `json:"menuItem,omitempty"`
Expand Down
67 changes: 57 additions & 10 deletions window.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package astilectron

import (
"net/url"
"sync"

"github.com/asticode/go-astilog"
"github.com/asticode/go-astitools/context"
"github.com/asticode/go-astitools/url"
"github.com/pkg/errors"
Expand All @@ -19,7 +21,8 @@ const (
EventNameWindowCmdHide = "window.cmd.hide"
EventNameWindowCmdLog = "window.cmd.log"
EventNameWindowCmdMaximize = "window.cmd.maximize"
EventNameWindowCmdMessage = "window.cmd.message"
eventNameWindowCmdMessage = "window.cmd.message"
eventNameWindowCmdMessageCallback = "window.cmd.message.callback"
EventNameWindowCmdMinimize = "window.cmd.minimize"
EventNameWindowCmdMove = "window.cmd.move"
EventNameWindowCmdResize = "window.cmd.resize"
Expand All @@ -34,7 +37,8 @@ const (
EventNameWindowEventFocus = "window.event.focus"
EventNameWindowEventHide = "window.event.hide"
EventNameWindowEventMaximize = "window.event.maximize"
EventNameWindowEventMessage = "window.event.message"
eventNameWindowEventMessage = "window.event.message"
eventNameWindowEventMessageCallback = "window.event.message.callback"
EventNameWindowEventMinimize = "window.event.minimize"
EventNameWindowEventMove = "window.event.move"
EventNameWindowEventReadyToShow = "window.event.ready.to.show"
Expand All @@ -58,9 +62,11 @@ var (
// TODO Add missing window events
type Window struct {
*object
o *WindowOptions
Session *Session
url *url.URL
callbackIdentifier *identifier
o *WindowOptions
onMessageOnce sync.Once
Session *Session
url *url.URL
}

// WindowOptions represents window options
Expand Down Expand Up @@ -149,8 +155,9 @@ type WebPreferences struct {
func newWindow(o Options, url string, wo *WindowOptions, c *asticontext.Canceller, d *dispatcher, i *identifier, wrt *writer) (w *Window, err error) {
// Init
w = &Window{
o: wo,
object: newObject(nil, c, d, i, wrt),
callbackIdentifier: newIdentifier(),
o: wo,
object: newObject(nil, c, d, i, wrt),
}
w.Session = newSession(w.ctx, c, d, i, wrt)

Expand Down Expand Up @@ -296,6 +303,29 @@ func (w *Window) MoveInDisplay(d *Display, x, y int) error {
return w.Move(d.Bounds().X+x, d.Bounds().Y+y)
}

// ListenerMessage represents a message listener executed when receiving a message from the JS
type ListenerMessage func(e Event) (v interface{})

// OnMessage adds a specific listener executed when receiving a message from the JS
// This method can be called only once
func (w *Window) OnMessage(l ListenerMessage) {
w.onMessageOnce.Do(func() {
w.On(eventNameWindowEventMessage, func(i Event) (deleteListener bool) {
v := l(i)
if len(i.CallbackID) > 0 {
o := Event{CallbackID: i.CallbackID, Name: eventNameWindowCmdMessageCallback, TargetID: w.id}
if v != nil {
o.Message = newEventMessage(v)
}
if err := w.w.write(o); err != nil {
astilog.Error(errors.Wrap(err, "writing callback message failed"))
}
}
return
})
})
}

// OpenDevTools opens the dev tools
func (w *Window) OpenDevTools() (err error) {
if err = w.isActionable(); err != nil {
Expand Down Expand Up @@ -324,12 +354,29 @@ func (w *Window) Restore() (err error) {
return
}

// Send sends a message to the inner JS of the Web content of the window
func (w *Window) Send(message interface{}) (err error) {
// CallbackMessage represents a message callback
type CallbackMessage func(e Event)

// SendMessage sends a message to the JS window and execute optional callbacks upon receiving a response from the JS
// Use astilectron.onMessage method to capture those messages in JS
func (w *Window) SendMessage(message interface{}, callbacks ...CallbackMessage) (err error) {
if err = w.isActionable(); err != nil {
return
}
return w.w.write(Event{Message: newEventMessage(message), Name: EventNameWindowCmdMessage, TargetID: w.id})
var e = Event{Message: newEventMessage(message), Name: eventNameWindowCmdMessage, TargetID: w.id}
if len(callbacks) > 0 {
e.CallbackID = w.callbackIdentifier.new()
w.On(eventNameWindowEventMessageCallback, func(i Event) (deleteListener bool) {
if i.CallbackID == e.CallbackID {
for _, c := range callbacks {
c(i)
}
deleteListener = true
}
return
})
}
return w.w.write(e)
}

// Show shows the window
Expand Down
43 changes: 42 additions & 1 deletion window_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package astilectron

import (
"sync"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -62,11 +63,51 @@ func TestWindow_Actions(t *testing.T) {
testObjectAction(t, func() error { return w.MoveInDisplay(d, 3, 4) }, w.object, wrt, "{\"name\":\""+EventNameWindowCmdMove+"\",\"targetID\":\""+w.id+"\",\"windowOptions\":{\"x\":4,\"y\":6}}\n", EventNameWindowEventMove)
testObjectAction(t, func() error { return w.Resize(1, 2) }, w.object, wrt, "{\"name\":\""+EventNameWindowCmdResize+"\",\"targetID\":\""+w.id+"\",\"windowOptions\":{\"height\":2,\"width\":1}}\n", EventNameWindowEventResize)
testObjectAction(t, func() error { return w.Restore() }, w.object, wrt, "{\"name\":\""+EventNameWindowCmdRestore+"\",\"targetID\":\""+w.id+"\"}\n", EventNameWindowEventRestore)
testObjectAction(t, func() error { return w.Send(true) }, w.object, wrt, "{\"name\":\""+EventNameWindowCmdMessage+"\",\"targetID\":\""+w.id+"\",\"message\":true}\n", "")
testObjectAction(t, func() error { return w.Show() }, w.object, wrt, "{\"name\":\""+EventNameWindowCmdShow+"\",\"targetID\":\""+w.id+"\"}\n", EventNameWindowEventShow)
testObjectAction(t, func() error { return w.Unmaximize() }, w.object, wrt, "{\"name\":\""+EventNameWindowCmdUnmaximize+"\",\"targetID\":\""+w.id+"\"}\n", EventNameWindowEventUnmaximize)
}

func TestWindow_OnMessage(t *testing.T) {
a, err := New(Options{})
assert.NoError(t, err)
defer a.Close()
wrt := &mockedWriter{wg: &sync.WaitGroup{}}
a.writer = newWriter(wrt)
w, err := a.NewWindow("http://test.com", &WindowOptions{})
assert.NoError(t, err)
w.OnMessage(func(e Event) interface{} {
return "test"
})
wrt.wg.Add(1)
a.dispatcher.dispatch(Event{CallbackID: "1", Name: eventNameWindowEventMessage, TargetID: w.id})
wrt.wg.Wait()
assert.Equal(t, []string{"{\"name\":\"window.cmd.message.callback\",\"targetID\":\"1\",\"callbackId\":\"1\",\"message\":\"test\"}\n"}, wrt.w)
}

func TestWindow_SendMessage(t *testing.T) {
a, err := New(Options{})
assert.NoError(t, err)
defer a.Close()
wrt := &mockedWriter{}
a.writer = newWriter(wrt)
w, err := a.NewWindow("http://test.com", &WindowOptions{})
assert.NoError(t, err)
wrt.fn = func() {
a.dispatcher.dispatch(Event{CallbackID: "1", Message: newEventMessage([]byte("\"bar\"")), Name: eventNameWindowEventMessageCallback, TargetID: w.id})
wrt.fn = nil
}
var wg sync.WaitGroup
wg.Add(1)
var s string
w.SendMessage("foo", func(e Event) {
e.Message.Unmarshal(&s)
wg.Done()
})
wg.Wait()
assert.Equal(t, []string{"{\"name\":\"window.cmd.message\",\"targetID\":\"1\",\"callbackId\":\"1\",\"message\":\"foo\"}\n"}, wrt.w)
assert.Equal(t, "bar", s)
}

func TestWindow_NewMenu(t *testing.T) {
a, err := New(Options{})
assert.NoError(t, err)
Expand Down
Loading

0 comments on commit d29e02b

Please sign in to comment.