Skip to content
This repository has been archived by the owner on Feb 16, 2024. It is now read-only.

Commit

Permalink
Implement global shortcuts (#393)
Browse files Browse the repository at this point in the history
* Implement global shortcuts

* No need to remove callback from map when unregistering a shortcut

The relevant event will no longer be triggered after the shortcut is unregistered. If the same key is registered again, the original callback will be replaced with new one.
Therefore, we don't need to find the registered key from the map. Besides, we don't need to parse the accelerator, too.

An error will be returned if the method failed.

* Add test case and doc

1. Add test case
2. Add doc to README.md
3. Handle `ctx.Err` in functions

* Updated

* Updated

* Updated

* Updated

* Updated

* Updated
  • Loading branch information
benebsiny committed Dec 21, 2023
1 parent 1082851 commit e91cd32
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 52 deletions.
21 changes: 21 additions & 0 deletions README.md
Expand Up @@ -446,6 +446,27 @@ var m = d.NewMenu([]*astilectron.MenuItemOptions{
m.Create()
```

## Global Shortcuts

Registering a global shortcut.

```go
// Register a new global shortcut
isRegistered, _ := a.GlobalShortcuts().Register("CmdOrCtrl+x", func() {
fmt.Println("CmdOrCtrl+x is pressed")
})
fmt.Println("CmdOrCtrl+x is registered:", isRegistered) // true

// Check if a global shortcut is registered
isRegistered, _ = a.GlobalShortcuts().IsRegistered("Shift+Y") // false

// Unregister a global shortcut
a.GlobalShortcuts().Unregister("CmdOrCtrl+x")

// Unregister all global shortcuts
a.GlobalShortcuts().UnregisterAll()
```

## Dialogs

Add the following line at the top of your javascript file :
Expand Down
46 changes: 27 additions & 19 deletions astilectron.go
Expand Up @@ -2,18 +2,17 @@ package astilectron

import (
"fmt"
"github.com/asticode/go-astikit"
"net"
"os/exec"
"runtime"
"time"

"github.com/asticode/go-astikit"
)

// Versions
const (
DefaultAcceptTCPTimeout = 30 * time.Second
DefaultVersionAstilectron = "0.57.0"
DefaultVersionAstilectron = "0.58.0"
DefaultVersionElectron = "11.4.3"
)

Expand Down Expand Up @@ -41,22 +40,23 @@ const (

// Astilectron represents an object capable of interacting with Astilectron
type Astilectron struct {
dispatcher *dispatcher
displayPool *displayPool
dock *Dock
executer Executer
identifier *identifier
l astikit.SeverityLogger
listener net.Listener
options Options
paths *Paths
provisioner Provisioner
reader *reader
stderrWriter *astikit.WriterAdapter
stdoutWriter *astikit.WriterAdapter
supported *Supported
worker *astikit.Worker
writer *writer
dispatcher *dispatcher
displayPool *displayPool
dock *Dock
executer Executer
globalShortcuts *GlobalShortcuts
identifier *identifier
l astikit.SeverityLogger
listener net.Listener
options Options
paths *Paths
provisioner Provisioner
reader *reader
stderrWriter *astikit.WriterAdapter
stdoutWriter *astikit.WriterAdapter
supported *Supported
worker *astikit.Worker
writer *writer
}

// Options represents Astilectron options
Expand Down Expand Up @@ -156,6 +156,11 @@ func (a *Astilectron) SetExecuter(e Executer) *Astilectron {
return a
}

// GlobalShortcuts gets the global shortcuts
func (a *Astilectron) GlobalShortcuts() *GlobalShortcuts {
return a.globalShortcuts
}

// On implements the Listenable interface
func (a *Astilectron) On(eventName string, l Listener) {
a.dispatcher.addListener(targetIDApp, eventName, l)
Expand Down Expand Up @@ -322,6 +327,9 @@ func (a *Astilectron) executeCmd(cmd *exec.Cmd) (err error) {
// Create dock
a.dock = newDock(a.worker.Context(), a.dispatcher, a.identifier, a.writer)

// Create global shortcuts
a.globalShortcuts = newGlobalShortcuts(a.worker.Context(), a.dispatcher, a.identifier, a.writer)

// Update supported features
a.supported = e.Supported
return
Expand Down
73 changes: 40 additions & 33 deletions event.go
Expand Up @@ -20,39 +20,40 @@ 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
AuthInfo *EventAuthInfo `json:"authInfo,omitempty"`
Badge *string `json:"badge,omitempty"`
BounceType string `json:"bounceType,omitempty"`
Bounds *RectangleOptions `json:"bounds,omitempty"`
CallbackID string `json:"callbackId,omitempty"`
Code string `json:"code,omitempty"`
Displays *EventDisplays `json:"displays,omitempty"`
Enable *bool `json:"enable,omitempty"`
FilePath string `json:"filePath,omitempty"`
ID *int `json:"id,omitempty"`
Image string `json:"image,omitempty"`
Index *int `json:"index,omitempty"`
Menu *EventMenu `json:"menu,omitempty"`
MenuItem *EventMenuItem `json:"menuItem,omitempty"`
MenuItemOptions *MenuItemOptions `json:"menuItemOptions,omitempty"`
MenuItemPosition *int `json:"menuItemPosition,omitempty"`
MenuPopupOptions *MenuPopupOptions `json:"menuPopupOptions,omitempty"`
Message *EventMessage `json:"message,omitempty"`
NotificationOptions *NotificationOptions `json:"notificationOptions,omitempty"`
Password string `json:"password,omitempty"`
Path string `json:"path,omitempty"`
Reply string `json:"reply,omitempty"`
Request *EventRequest `json:"request,omitempty"`
SecondInstance *EventSecondInstance `json:"secondInstance,omitempty"`
SessionID string `json:"sessionId,omitempty"`
Supported *Supported `json:"supported,omitempty"`
TrayOptions *TrayOptions `json:"trayOptions,omitempty"`
URL string `json:"url,omitempty"`
URLNew string `json:"newUrl,omitempty"`
URLOld string `json:"oldUrl,omitempty"`
Username string `json:"username,omitempty"`
WindowID string `json:"windowId,omitempty"`
WindowOptions *WindowOptions `json:"windowOptions,omitempty"`
AuthInfo *EventAuthInfo `json:"authInfo,omitempty"`
Badge *string `json:"badge,omitempty"`
BounceType string `json:"bounceType,omitempty"`
Bounds *RectangleOptions `json:"bounds,omitempty"`
CallbackID string `json:"callbackId,omitempty"`
Code string `json:"code,omitempty"`
Displays *EventDisplays `json:"displays,omitempty"`
Enable *bool `json:"enable,omitempty"`
FilePath string `json:"filePath,omitempty"`
GlobalShortcuts *EventGlobalShortcuts `json:"globalShortcuts,omitempty"`
ID *int `json:"id,omitempty"`
Image string `json:"image,omitempty"`
Index *int `json:"index,omitempty"`
Menu *EventMenu `json:"menu,omitempty"`
MenuItem *EventMenuItem `json:"menuItem,omitempty"`
MenuItemOptions *MenuItemOptions `json:"menuItemOptions,omitempty"`
MenuItemPosition *int `json:"menuItemPosition,omitempty"`
MenuPopupOptions *MenuPopupOptions `json:"menuPopupOptions,omitempty"`
Message *EventMessage `json:"message,omitempty"`
NotificationOptions *NotificationOptions `json:"notificationOptions,omitempty"`
Password string `json:"password,omitempty"`
Path string `json:"path,omitempty"`
Reply string `json:"reply,omitempty"`
Request *EventRequest `json:"request,omitempty"`
SecondInstance *EventSecondInstance `json:"secondInstance,omitempty"`
SessionID string `json:"sessionId,omitempty"`
Supported *Supported `json:"supported,omitempty"`
TrayOptions *TrayOptions `json:"trayOptions,omitempty"`
URL string `json:"url,omitempty"`
URLNew string `json:"newUrl,omitempty"`
URLOld string `json:"oldUrl,omitempty"`
Username string `json:"username,omitempty"`
WindowID string `json:"windowId,omitempty"`
WindowOptions *WindowOptions `json:"windowOptions,omitempty"`
}

// EventAuthInfo represents an event auth info
Expand All @@ -70,6 +71,12 @@ type EventDisplays struct {
Primary *DisplayOptions `json:"primary,omitempty"`
}

// EventGlobalShortcuts represents event global shortcuts
type EventGlobalShortcuts struct {
Accelerator string `json:"accelerator,omitempty"`
IsRegistered bool `json:"isRegistered,omitempty"`
}

// EventMessage represents an event message
type EventMessage struct {
i interface{}
Expand Down
123 changes: 123 additions & 0 deletions global_shortcuts.go
@@ -0,0 +1,123 @@
package astilectron

import (
"context"
"sync"
)

const (
EventNameGlobalShortcutsCmdRegister = "global.shortcuts.cmd.register"
EventNameGlobalShortcutsCmdIsRegistered = "global.shortcuts.cmd.is.registered"
EventNameGlobalShortcutsCmdUnregister = "global.shortcuts.cmd.unregister"
EventNameGlobalShortcutsCmdUnregisterAll = "global.shortcuts.cmd.unregister.all"
EventNameGlobalShortcutsEventRegistered = "global.shortcuts.event.registered"
EventNameGlobalShortcutsEventIsRegistered = "global.shortcuts.event.is.registered"
EventNameGlobalShortcutsEventUnregistered = "global.shortcuts.event.unregistered"
EventNameGlobalShortcutsEventUnregisteredAll = "global.shortcuts.event.unregistered.all"
EventNameGlobalShortcutEventTriggered = "global.shortcuts.event.triggered"
)

type globalShortcutsCallback func()

// GlobalShortcuts represents global shortcuts
type GlobalShortcuts struct {
*object
m *sync.Mutex
callbacks map[string]globalShortcutsCallback
}

func newGlobalShortcuts(ctx context.Context, d *dispatcher, i *identifier, w *writer) (gs *GlobalShortcuts) {
gs = &GlobalShortcuts{
object: newObject(ctx, d, i, w, i.new()),
m: new(sync.Mutex),
callbacks: make(map[string]globalShortcutsCallback),
}
gs.On(EventNameGlobalShortcutEventTriggered, func(e Event) (deleteListener bool) { // Register the listener for the triggered event
gs.m.Lock()
callback, ok := gs.callbacks[e.GlobalShortcuts.Accelerator]
gs.m.Unlock()
if ok {
(callback)()
}
return
})
return
}

// Register registers a global shortcut
func (gs *GlobalShortcuts) Register(accelerator string, callback globalShortcutsCallback) (isRegistered bool, err error) {
if err = gs.ctx.Err(); err != nil {
return
}

// Send an event to astilectron to register the global shortcut
result, err := synchronousEvent(gs.ctx, gs, gs.w, Event{Name: EventNameGlobalShortcutsCmdRegister, TargetID: gs.id, GlobalShortcuts: &EventGlobalShortcuts{Accelerator: accelerator}}, EventNameGlobalShortcutsEventRegistered)
if err != nil {
return
}

// If registered successfully, add the callback to the map
if result.GlobalShortcuts != nil {
if result.GlobalShortcuts.IsRegistered {
gs.m.Lock()
gs.callbacks[accelerator] = callback
gs.m.Unlock()
}
isRegistered = result.GlobalShortcuts.IsRegistered
}
return
}

// IsRegistered checks whether a global shortcut is registered
func (gs *GlobalShortcuts) IsRegistered(accelerator string) (isRegistered bool, err error) {
if err = gs.ctx.Err(); err != nil {
return
}

// Send an event to astilectron to check if global shortcut is registered
result, err := synchronousEvent(gs.ctx, gs, gs.w, Event{Name: EventNameGlobalShortcutsCmdIsRegistered, TargetID: gs.id, GlobalShortcuts: &EventGlobalShortcuts{Accelerator: accelerator}}, EventNameGlobalShortcutsEventIsRegistered)
if err != nil {
return
}

if result.GlobalShortcuts != nil {
isRegistered = result.GlobalShortcuts.IsRegistered
}
return
}

// Unregister unregisters a global shortcut
func (gs *GlobalShortcuts) Unregister(accelerator string) (err error) {
if err = gs.ctx.Err(); err != nil {
return
}

// Send an event to astilectron to unregister the global shortcut
_, err = synchronousEvent(gs.ctx, gs, gs.w, Event{Name: EventNameGlobalShortcutsCmdUnregister, TargetID: gs.id, GlobalShortcuts: &EventGlobalShortcuts{Accelerator: accelerator}}, EventNameGlobalShortcutsEventUnregistered)
if err != nil {
return
}
gs.m.Lock()
delete(gs.callbacks, accelerator)
gs.m.Unlock()
return
}

// UnregisterAll unregisters all global shortcuts
func (gs *GlobalShortcuts) UnregisterAll() (err error) {
if err = gs.ctx.Err(); err != nil {
return
}

// Send an event to astilectron to unregister all global shortcuts
_, err = synchronousEvent(gs.ctx, gs, gs.w, Event{Name: EventNameGlobalShortcutsCmdUnregisterAll, TargetID: gs.id}, EventNameGlobalShortcutsEventUnregisteredAll)
if err != nil {
return
}

gs.m.Lock()
gs.callbacks = make(map[string]globalShortcutsCallback) // Clear the map
gs.m.Unlock()

return
}
42 changes: 42 additions & 0 deletions global_shortcuts_test.go
@@ -0,0 +1,42 @@
package astilectron

import (
"context"
"fmt"
"testing"
)

func TestGlobalShortcut_Actions(t *testing.T) {
var d = newDispatcher()
var i = newIdentifier()
var wrt = &mockedWriter{}
var w = newWriter(wrt, &logger{})

var gs = newGlobalShortcuts(context.Background(), d, i, w)

// Register
testObjectAction(t, func() error {
_, e := gs.Register("Ctrl+X", func() {})
return e
}, gs.object, wrt, fmt.Sprintf(`{"name":"%s","targetID":"%s","globalShortcuts":{"accelerator":"Ctrl+X"}}%s`, EventNameGlobalShortcutsCmdRegister, gs.id, "\n"),
EventNameGlobalShortcutsEventRegistered, &Event{GlobalShortcuts: &EventGlobalShortcuts{Accelerator: "Ctrl+X", IsRegistered: true}})

// IsRegistered
testObjectAction(t, func() error {
_, e := gs.IsRegistered("Ctrl+Y")
return e
}, gs.object, wrt, fmt.Sprintf(`{"name":"%s","targetID":"%s","globalShortcuts":{"accelerator":"Ctrl+Y"}}%s`, EventNameGlobalShortcutsCmdIsRegistered, gs.id, "\n"),
EventNameGlobalShortcutsEventIsRegistered, &Event{GlobalShortcuts: &EventGlobalShortcuts{Accelerator: "Ctrl+Y", IsRegistered: false}})

// Unregister
testObjectAction(t, func() error {
return gs.Unregister("Ctrl+Z")
}, gs.object, wrt, fmt.Sprintf(`{"name":"%s","targetID":"%s","globalShortcuts":{"accelerator":"Ctrl+Z"}}%s`, EventNameGlobalShortcutsCmdUnregister, gs.id, "\n"),
EventNameGlobalShortcutsEventUnregistered, nil)

// UnregisterAll
testObjectAction(t, func() error {
return gs.UnregisterAll()
}, gs.object, wrt, fmt.Sprintf(`{"name":"%s","targetID":"%s"}%s`, EventNameGlobalShortcutsCmdUnregisterAll, gs.id, "\n"),
EventNameGlobalShortcutsEventUnregisteredAll, nil)
}

0 comments on commit e91cd32

Please sign in to comment.