diff --git a/.gitignore b/.gitignore
index ae7e06b..2e85ef6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,8 @@
example/example
+webview_example/webview_example
*~
*.swp
-*.exe
+**/*.exe
Release
Debug
*.sdf
diff --git a/README.md b/README.md
index be16646..3e00f32 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,15 @@
-Package systray is a cross platfrom Go library to place an icon and menu in the notification area.
-Tested on Windows 8, Mac OSX, Ubuntu 14.10 and Debian 7.6.
+systray is a cross-platform Go library to place an icon and menu in the notification area.
+
+## Features
+
+* Supported on Windows, macOS, and Linux
+* Menu items can be checked and/or disabled
+* Methods may be called from any Goroutine
+
+## API
-## Usage
```go
func main() {
- // Should be called at the very beginning of main().
systray.Run(onReady, onExit)
}
@@ -14,7 +19,7 @@ func onReady() {
systray.SetTooltip("Pretty awesome超级棒")
mQuit := systray.AddMenuItem("Quit", "Quit the whole app")
- // Sets the icon of a menu item. Only available on Mac.
+ // Sets the icon of a menu item. Only available on Mac and Windows.
mQuit.SetIcon(icon.Data)
}
@@ -22,35 +27,61 @@ func onExit() {
// clean up here
}
```
-Menu item can be checked and / or disabled. Methods except `Run()` can be invoked from any goroutine. See demo code under `example` folder.
-## Platform specific concerns
+## Try the example app!
+
+Have go v1.12+ or higher installed? Here's an example to get started on macOS:
+
+```sh
+git clone https://github.com/getlantern/systray
+cd example
+env GO111MODULE=on go build
+./example
+```
+
+On Windows, you should build like this:
+
+```
+env GO111MODULE=on go build -ldflags "-H=windowsgui"
+```
+
+The following text will then appear on the console:
+
+
+```sh
+go: finding github.com/skratchdot/open-golang latest
+go: finding github.com/getlantern/systray latest
+go: finding github.com/getlantern/golog latest
+```
+
+Now look for *Awesome App* in your menu bar!
+
+
+
+## The Webview example
+
+The code under `webview_example` is to demostrate how it can co-exist with other UI elements.
+
+## Platform notes
### Linux
-```sh
-sudo apt-get install libgtk-3-dev libappindicator3-dev
-```
-Checked menu item not implemented on Linux yet.
-
-## Try
-
-Under `example` folder.
-Place tray icon under `icon`, and use `make_icon.bat` or `make_icon.sh`, whichever suit for your os, to convert the icon to byte array.
-Your icon should be .ico file under Windows, whereas .ico, .jpg and .png is supported on other platform.
+* Building apps requires gcc as well as the `gtk3` and `libappindicator3` development headers to be installed. For Debian or Ubuntu, you may install these using:
```sh
-go get
-go run main.go
+sudo apt-get install gcc libgtk-3-dev libappindicator3-dev
```
-## Building and the Console Window
+On Linux Mint, `libxapp-dev` is also required .
+
+To build `webview_example`, you also need to install `libwebkit2gtk-4.0-dev` and remove `webview_example/rsrc.syso` which is required on Windows.
+
+* Submenu and checked menu items are not yet implemented
-By default, the binary created by `go build` will cause a console window to be opened on both Windows and macOS when run.
### Windows
-To prevent launching a console window when running on Windows, add these command-line build flags:
+* To avoid opening a console at application startup, use these compile flags:
```sh
go build -ldflags -H=windowsgui
@@ -70,6 +101,18 @@ SystrayApp.app/
SystrayApp.icns
```
+When running as an app bundle, you may want to add one or both of the following to your Info.plist:
+
+```xml
+
+ NSHighResolutionCapable
+ True
+
+
+ LSUIElement
+ 1
+```
+
Consult the [Official Apple Documentation here](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW1).
## Credits
diff --git a/example/example_windows_386.exe b/example/example_windows_386.exe
index b680331..3d33c6e 100755
Binary files a/example/example_windows_386.exe and b/example/example_windows_386.exe differ
diff --git a/example/main.go b/example/main.go
index 5542b89..8a02fed 100644
--- a/example/main.go
+++ b/example/main.go
@@ -12,17 +12,15 @@ import (
func main() {
onExit := func() {
- fmt.Println("Starting onExit")
now := time.Now()
ioutil.WriteFile(fmt.Sprintf(`on_exit_%d.txt`, now.UnixNano()), []byte(now.String()), 0644)
- fmt.Println("Finished onExit")
}
- // Should be called at the very beginning of main().
+
systray.Run(onReady, onExit)
}
func onReady() {
- systray.SetIcon(icon.Data)
+ systray.SetTemplateIcon(icon.Data, icon.Data)
systray.SetTitle("Awesome App")
systray.SetTooltip("Lantern")
mQuitOrig := systray.AddMenuItem("Quit", "Quit the whole app")
@@ -35,14 +33,23 @@ func onReady() {
// We can manipulate the systray in other goroutines
go func() {
- systray.SetIcon(icon.Data)
+ systray.SetTemplateIcon(icon.Data, icon.Data)
systray.SetTitle("Awesome App")
systray.SetTooltip("Pretty awesome棒棒嗒")
mChange := systray.AddMenuItem("Change Me", "Change Me")
mChecked := systray.AddMenuItem("Unchecked", "Check Me")
mEnabled := systray.AddMenuItem("Enabled", "Enabled")
+ // Sets the icon of a menu item. Only available on Mac.
+ mEnabled.SetTemplateIcon(icon.Data, icon.Data)
+
systray.AddMenuItem("Ignored", "Ignored")
- mUrl := systray.AddMenuItem("Open Lantern.org", "my home")
+
+ subMenuTop := systray.AddMenuItem("SubMenu", "SubMenu Test (top)")
+ subMenuMiddle := subMenuTop.AddSubMenuItem("SubMenu - Level 2", "SubMenu Test (middle)")
+ subMenuBottom := subMenuMiddle.AddSubMenuItem("SubMenu - Level 3", "SubMenu Test (bottom)")
+ subMenuBottom2 := subMenuMiddle.AddSubMenuItem("Panic!", "SubMenu Test (bottom)")
+
+ mUrl := systray.AddMenuItem("Open UI", "my home")
mQuit := systray.AddMenuItem("退出", "Quit the whole app")
// Sets the icon of a menu item. Only available on Mac.
@@ -51,6 +58,22 @@ func onReady() {
systray.AddSeparator()
mToggle := systray.AddMenuItem("Toggle", "Toggle the Quit button")
shown := true
+ toggle := func() {
+ if shown {
+ subMenuBottom.Check()
+ subMenuBottom2.Hide()
+ mQuitOrig.Hide()
+ mEnabled.Hide()
+ shown = false
+ } else {
+ subMenuBottom.Uncheck()
+ subMenuBottom2.Show()
+ mQuitOrig.Show()
+ mEnabled.Show()
+ shown = true
+ }
+ }
+
for {
select {
case <-mChange.ClickedCh:
@@ -68,16 +91,12 @@ func onReady() {
mEnabled.Disable()
case <-mUrl.ClickedCh:
open.Run("https://www.getlantern.org")
+ case <-subMenuBottom2.ClickedCh:
+ panic("panic button pressed")
+ case <-subMenuBottom.ClickedCh:
+ toggle()
case <-mToggle.ClickedCh:
- if shown {
- mQuitOrig.Hide()
- mEnabled.Hide()
- shown = false
- } else {
- mQuitOrig.Show()
- mEnabled.Show()
- shown = true
- }
+ toggle()
case <-mQuit.ClickedCh:
systray.Quit()
fmt.Println("Quit2 now...")
diff --git a/example/screenshot.png b/example/screenshot.png
new file mode 100644
index 0000000..fb3ab91
Binary files /dev/null and b/example/screenshot.png differ
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..d0a8fc3
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,12 @@
+module github.com/getlantern/systray
+
+go 1.13
+
+require (
+ github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7
+ github.com/lxn/walk v0.0.0-20191128110447-55ccb3a9f5c1
+ github.com/lxn/win v0.0.0-20191128105842-2da648fda5b4 // indirect
+ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
+ golang.org/x/sys v0.0.0-20200513112337-417ce2331b5c
+ gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..8be5240
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,34 @@
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
+github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
+github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
+github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
+github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
+github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
+github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
+github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
+github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
+github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
+github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
+github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
+github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/lxn/walk v0.0.0-20191128110447-55ccb3a9f5c1 h1:/QwQcwWVOQXcoNuV9tHx30gQ3q7jCE/rKcGjwzsa5tg=
+github.com/lxn/walk v0.0.0-20191128110447-55ccb3a9f5c1/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
+github.com/lxn/win v0.0.0-20191128105842-2da648fda5b4 h1:5BmtGkQbch91lglMHQ9JIDGiYCL3kBRBA0ItZTvOcEI=
+github.com/lxn/win v0.0.0-20191128105842-2da648fda5b4/go.mod h1:ouWl4wViUNh8tPSIwxTVMuS014WakR1hqvBc2I0bMoA=
+github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
+github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
+github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200513112337-417ce2331b5c h1:kISX68E8gSkNYAFRFiDU8rl5RIn1sJYKYb/r2vMLDrU=
+golang.org/x/sys v0.0.0-20200513112337-417ce2331b5c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc=
+gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
diff --git a/systray.go b/systray.go
index b4970d5..987e9d2 100644
--- a/systray.go
+++ b/systray.go
@@ -1,13 +1,10 @@
/*
-Package systray is a cross platfrom Go library to place an icon and menu in the
-notification area.
-Supports Windows, Mac OSX and Linux currently.
-Methods can be called from any goroutine except Run(), which should be called
-at the very beginning of main() to lock at main thread.
+Package systray is a cross-platform Go library to place an icon and menu in the notification area.
*/
package systray
import (
+ "fmt"
"runtime"
"sync"
"sync/atomic"
@@ -16,11 +13,22 @@ import (
)
var (
- hasStarted = int64(0)
- hasQuit = int64(0)
+ log = golog.LoggerFor("systray")
+
+ systrayReady func()
+ systrayExit func()
+ menuItems = make(map[int32]*MenuItem)
+ menuItemsLock sync.RWMutex
+
+ currentID = int32(-1)
+ quitOnce sync.Once
)
-// MenuItem is used to keep track each menu item of systray
+func init() {
+ runtime.LockOSThread()
+}
+
+// MenuItem is used to keep track each menu item of systray.
// Don't create it directly, use the one systray.AddMenuItem() returned
type MenuItem struct {
// ClickedCh is the channel which will be notified when the menu item is clicked
@@ -36,27 +44,41 @@ type MenuItem struct {
disabled bool
// checked menu item has a tick before the title
checked bool
+ // parent item, for sub menus
+ parent *MenuItem
}
-var (
- log = golog.LoggerFor("systray")
+func (item *MenuItem) String() string {
+ if item.parent == nil {
+ return fmt.Sprintf("MenuItem[%d, %q]", item.id, item.title)
+ }
+ return fmt.Sprintf("MenuItem[%d, parent %d, %q]", item.id, item.parent.id, item.title)
+}
- systrayReady func()
- systrayExit func()
- menuItems = make(map[int32]*MenuItem)
- menuItemsLock sync.RWMutex
-
- currentID = int32(-1)
-)
+// newMenuItem returns a populated MenuItem object
+func newMenuItem(title string, tooltip string, parent *MenuItem) *MenuItem {
+ return &MenuItem{
+ ClickedCh: make(chan struct{}),
+ id: atomic.AddInt32(¤tID, 1),
+ title: title,
+ tooltip: tooltip,
+ disabled: false,
+ checked: false,
+ parent: parent,
+ }
+}
// Run initializes GUI and starts the event loop, then invokes the onReady
-// callback.
-// It blocks until systray.Quit() is called.
-// Should be called at the very beginning of main() to lock at main thread.
+// callback. It blocks until systray.Quit() is called.
func Run(onReady func(), onExit func()) {
- runtime.LockOSThread()
- atomic.StoreInt64(&hasStarted, 1)
+ Register(onReady, onExit)
+ nativeLoop()
+}
+// Register initializes GUI and registers the callbacks but relies on the
+// caller to run the event loop somewhere else. It's useful if the program
+// needs to show other UI elements, for example, webview.
+func Register(onReady func(), onExit func()) {
if onReady == nil {
systrayReady = func() {}
} else {
@@ -70,32 +92,25 @@ func Run(onReady func(), onExit func()) {
close(readyCh)
}
}
-
// unlike onReady, onExit runs in the event loop to make sure it has time to
// finish before the process terminates
if onExit == nil {
onExit = func() {}
}
systrayExit = onExit
-
- nativeLoop()
+ registerSystray()
}
// Quit the systray
func Quit() {
- if atomic.LoadInt64(&hasStarted) == 1 && atomic.CompareAndSwapInt64(&hasQuit, 0, 1) {
- quit()
- }
+ quitOnce.Do(quit)
}
-// AddMenuItem adds menu item with designated title and tooltip, returning a channel
-// that notifies whenever that menu item is clicked.
+// AddMenuItem adds a menu item with the designated title and tooltip.
//
// It can be safely invoked from different goroutines.
func AddMenuItem(title string, tooltip string) *MenuItem {
- id := atomic.AddInt32(¤tID, 1)
- item := &MenuItem{nil, id, title, tooltip, false, false}
- item.ClickedCh = make(chan struct{})
+ item := newMenuItem(title, tooltip, nil)
item.update()
return item
}
@@ -105,6 +120,14 @@ func AddSeparator() {
addSeparator(atomic.AddInt32(¤tID, 1))
}
+// AddSubMenuItem adds a nested sub-menu item with the designated title and tooltip.
+// It can be safely invoked from different goroutines.
+func (item *MenuItem) AddSubMenuItem(title string, tooltip string) *MenuItem {
+ child := newMenuItem(title, tooltip, item)
+ child.update()
+ return child
+}
+
// SetTitle set the text to display on a menu item
func (item *MenuItem) SetTitle(title string) {
item.title = title
@@ -117,7 +140,7 @@ func (item *MenuItem) SetTooltip(tooltip string) {
item.update()
}
-// Disabled checkes if the menu item is disabled
+// Disabled checks if the menu item is disabled
func (item *MenuItem) Disabled() bool {
return item.disabled
}
@@ -161,7 +184,7 @@ func (item *MenuItem) Uncheck() {
item.update()
}
-// update propogates changes on a menu item to systray
+// update propagates changes on a menu item to systray
func (item *MenuItem) update() {
menuItemsLock.Lock()
defer menuItemsLock.Unlock()
diff --git a/systray.h b/systray.h
index 36bcf98..e1a1f98 100644
--- a/systray.h
+++ b/systray.h
@@ -1,13 +1,16 @@
+#include "stdbool.h"
+
extern void systray_ready();
extern void systray_on_exit();
extern void systray_menu_item_selected(int menu_id);
+void registerSystray(void);
int nativeLoop(void);
-void setIcon(const char* iconBytes, int length);
-void setMenuItemIcon(const char* iconBytes, int length, int menuId);
+void setIcon(const char* iconBytes, int length, bool template);
+void setMenuItemIcon(const char* iconBytes, int length, int menuId, bool template);
void setTitle(char* title);
void setTooltip(char* tooltip);
-void add_or_update_menu_item(int menuId, char* title, char* tooltip, short disabled, short checked);
+void add_or_update_menu_item(int menuId, int parentMenuId, char* title, char* tooltip, short disabled, short checked);
void add_separator(int menuId);
void hide_menu_item(int menuId);
void show_menu_item(int menuId);
diff --git a/systray_darwin.go b/systray_darwin.go
new file mode 100644
index 0000000..568edab
--- /dev/null
+++ b/systray_darwin.go
@@ -0,0 +1,37 @@
+package systray
+
+/*
+#cgo darwin CFLAGS: -DDARWIN -x objective-c -fobjc-arc
+#cgo darwin LDFLAGS: -framework Cocoa -framework WebKit
+
+#include "systray.h"
+*/
+import "C"
+
+import (
+ "unsafe"
+)
+
+// SetTemplateIcon sets the systray icon as a template icon (on Mac), falling back
+// to a regular icon on other platforms.
+// templateIconBytes and regularIconBytes should be the content of .ico for windows and
+// .ico/.jpg/.png for other platforms.
+func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) {
+ cstr := (*C.char)(unsafe.Pointer(&templateIconBytes[0]))
+ C.setIcon(cstr, (C.int)(len(templateIconBytes)), true)
+}
+
+// SetIcon sets the icon of a menu item. Only works on macOS and Windows.
+// iconBytes should be the content of .ico/.jpg/.png
+func (item *MenuItem) SetIcon(iconBytes []byte) {
+ SetTemplateIcon(iconBytes, iconBytes)
+}
+
+// SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows, it
+// falls back to the regular icon bytes and on Linux it does nothing.
+// templateIconBytes and regularIconBytes should be the content of .ico for windows and
+// .ico/.jpg/.png for other platforms.
+func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) {
+ cstr := (*C.char)(unsafe.Pointer(&templateIconBytes[0]))
+ C.setMenuItemIcon(cstr, (C.int)(len(templateIconBytes)), C.int(item.id), true)
+}
\ No newline at end of file
diff --git a/systray_darwin.m b/systray_darwin.m
index b0f2172..b4f38bc 100644
--- a/systray_darwin.m
+++ b/systray_darwin.m
@@ -17,12 +17,14 @@
{
@public
NSNumber* menuId;
+ NSNumber* parentMenuId;
NSString* title;
NSString* tooltip;
short disabled;
short checked;
}
-(id) initWithId: (int)theMenuId
+withParentMenuId: (int)theParentMenuId
withTitle: (const char*)theTitle
withTooltip: (const char*)theTooltip
withDisabled: (short)theDisabled
@@ -30,12 +32,14 @@
@end
@implementation MenuItem
-(id) initWithId: (int)theMenuId
+ withParentMenuId: (int)theParentMenuId
withTitle: (const char*)theTitle
withTooltip: (const char*)theTooltip
withDisabled: (short)theDisabled
withChecked: (short)theChecked
{
menuId = [NSNumber numberWithInt:theMenuId];
+ parentMenuId = [NSNumber numberWithInt:theParentMenuId];
title = [[NSString alloc] initWithCString:theTitle
encoding:NSUTF8StringEncoding];
tooltip = [[NSString alloc] initWithCString:theTooltip
@@ -108,20 +112,30 @@
systray_menu_item_selected(menuId.intValue);
}
-- (void) add_or_update_menu_item:(MenuItem*) item
-{
- NSMenuItem* menuItem;
- int existedMenuIndex = [menu indexOfItemWithRepresentedObject: item->menuId];
- if (existedMenuIndex == -1) {
- menuItem = [menu addItemWithTitle:item->title action:@selector(menuHandler:) keyEquivalent:@""];
- [menuItem setTarget:self];
- [menuItem setRepresentedObject: item->menuId];
-
+- (void)add_or_update_menu_item:(MenuItem *)item {
+ NSMenu *theMenu = self->menu;
+ NSMenuItem *parentItem;
+ if ([item->parentMenuId integerValue] > 0) {
+ parentItem = find_menu_item(menu, item->parentMenuId);
+ if (parentItem.hasSubmenu) {
+ theMenu = parentItem.submenu;
+ } else {
+ theMenu = [[NSMenu alloc] init];
+ [parentItem setSubmenu:theMenu];
+ }
}
- else {
- menuItem = [menu itemAtIndex: existedMenuIndex];
- [menuItem setTitle:item->title];
+
+ NSMenuItem *menuItem;
+ menuItem = find_menu_item(theMenu, item->menuId);
+ if (menuItem == NULL) {
+ menuItem = [theMenu addItemWithTitle:item->title
+ action:@selector(menuHandler:)
+ keyEquivalent:@""];
+ [menuItem setRepresentedObject:item->menuId];
}
+ [menuItem setTitle:item->title];
+ [menuItem setTag:[item->menuId integerValue]];
+ [menuItem setTarget:self];
[menuItem setToolTip:item->tooltip];
if (item->disabled == 1) {
menuItem.enabled = FALSE;
@@ -135,6 +149,26 @@
}
}
+NSMenuItem *find_menu_item(NSMenu *ourMenu, NSNumber *menuId) {
+ NSMenuItem *foundItem = [ourMenu itemWithTag:[menuId integerValue]];
+ if (foundItem != NULL) {
+ return foundItem;
+ }
+ NSArray *menu_items = ourMenu.itemArray;
+ int i;
+ for (i = 0; i < [menu_items count]; i++) {
+ NSMenuItem *i_item = [menu_items objectAtIndex:i];
+ if (i_item.hasSubmenu) {
+ foundItem = find_menu_item(i_item.submenu, menuId);
+ if (foundItem != NULL) {
+ return foundItem;
+ }
+ }
+ }
+
+ return NULL;
+};
+
- (void) add_separator:(NSNumber*) menuId
{
[menu addItem: [NSMenuItem separatorItem]];
@@ -142,37 +176,30 @@
- (void) hide_menu_item:(NSNumber*) menuId
{
- NSMenuItem* menuItem;
- int existedMenuIndex = [menu indexOfItemWithRepresentedObject: menuId];
- if (existedMenuIndex == -1) {
- return;
+ NSMenuItem* menuItem = find_menu_item(menu, menuId);
+ if (menuItem != NULL) {
+ [menuItem setHidden:TRUE];
}
- menuItem = [menu itemAtIndex: existedMenuIndex];
- [menuItem setHidden:TRUE];
}
-- (void)setMenuItemIcon:(NSArray*)imageAndMenuId {
+- (void) setMenuItemIcon:(NSArray*)imageAndMenuId {
NSImage* image = [imageAndMenuId objectAtIndex:0];
NSNumber* menuId = [imageAndMenuId objectAtIndex:1];
NSMenuItem* menuItem;
- int existedMenuIndex = [menu indexOfItemWithRepresentedObject: menuId];
- if (existedMenuIndex == -1) {
+ menuItem = find_menu_item(menu, menuId);
+ if (menuItem == NULL) {
return;
}
- menuItem = [menu itemAtIndex: existedMenuIndex];
menuItem.image = image;
}
- (void) show_menu_item:(NSNumber*) menuId
{
- NSMenuItem* menuItem;
- int existedMenuIndex = [menu indexOfItemWithRepresentedObject: menuId];
- if (existedMenuIndex == -1) {
- return;
+ NSMenuItem* menuItem = find_menu_item(menu, menuId);
+ if (menuItem != NULL) {
+ [menuItem setHidden:FALSE];
}
- menuItem = [menu itemAtIndex: existedMenuIndex];
- [menuItem setHidden:FALSE];
}
- (void) quit
@@ -182,9 +209,12 @@
@end
-int nativeLoop(void) {
+void registerSystray(void) {
AppDelegate *delegate = [[AppDelegate alloc] init];
[[NSApplication sharedApplication] setDelegate:delegate];
+}
+
+int nativeLoop(void) {
[NSApp run];
return EXIT_SUCCESS;
}
@@ -196,18 +226,19 @@ void runInMainThread(SEL method, id object) {
waitUntilDone: YES];
}
-void setIcon(const char* iconBytes, int length) {
+void setIcon(const char* iconBytes, int length, bool template) {
NSData* buffer = [NSData dataWithBytes: iconBytes length:length];
NSImage *image = [[NSImage alloc] initWithData:buffer];
[image setSize:NSMakeSize(16, 16)];
+ image.template = template;
runInMainThread(@selector(setIcon:), (id)image);
}
-void setMenuItemIcon(const char* iconBytes, int length, int menuId) {
+void setMenuItemIcon(const char* iconBytes, int length, int menuId, bool template) {
NSData* buffer = [NSData dataWithBytes: iconBytes length:length];
NSImage *image = [[NSImage alloc] initWithData:buffer];
[image setSize:NSMakeSize(16, 16)];
-
+ image.template = template;
NSNumber *mId = [NSNumber numberWithInt:menuId];
runInMainThread(@selector(setMenuItemIcon:), @[image, (id)mId]);
}
@@ -226,8 +257,8 @@ void setTooltip(char* ctooltip) {
runInMainThread(@selector(setTooltip:), (id)tooltip);
}
-void add_or_update_menu_item(int menuId, char* title, char* tooltip, short disabled, short checked) {
- MenuItem* item = [[MenuItem alloc] initWithId: menuId withTitle: title withTooltip: tooltip withDisabled: disabled withChecked: checked];
+void add_or_update_menu_item(int menuId, int parentMenuId, char* title, char* tooltip, short disabled, short checked) {
+ MenuItem* item = [[MenuItem alloc] initWithId: menuId withParentMenuId: parentMenuId withTitle: title withTooltip: tooltip withDisabled: disabled withChecked: checked];
free(title);
free(tooltip);
runInMainThread(@selector(add_or_update_menu_item:), (id)item);
diff --git a/systray_linux.c b/systray_linux.c
index 72cd614..7586f1b 100644
--- a/systray_linux.c
+++ b/systray_linux.c
@@ -23,7 +23,7 @@ typedef struct {
short checked;
} MenuItemInfo;
-int nativeLoop(void) {
+void registerSystray(void) {
gtk_init(0, NULL);
global_app_indicator = app_indicator_new("systray", "",
APP_INDICATOR_CATEGORY_APPLICATION_STATUS);
@@ -31,6 +31,9 @@ int nativeLoop(void) {
global_tray_menu = gtk_menu_new();
app_indicator_set_menu(global_app_indicator, GTK_MENU(global_tray_menu));
systray_ready();
+}
+
+int nativeLoop(void) {
gtk_main();
systray_on_exit();
return 0;
@@ -167,7 +170,7 @@ gboolean do_quit(gpointer data) {
return FALSE;
}
-void setIcon(const char* iconBytes, int length) {
+void setIcon(const char* iconBytes, int length, bool template) {
GBytes* bytes = g_bytes_new_static(iconBytes, length);
g_idle_add(do_set_icon, bytes);
}
@@ -181,10 +184,11 @@ void setTooltip(char* ctooltip) {
free(ctooltip);
}
-void setMenuItemIcon(const char* iconBytes, int length, int menuId) {
+void setMenuItemIcon(const char* iconBytes, int length, int menuId, bool template) {
}
-void add_or_update_menu_item(int menu_id, char* title, char* tooltip, short disabled, short checked) {
+void add_or_update_menu_item(int menu_id, int parent_menu_id, char* title, char* tooltip, short disabled, short checked) {
+ // TODO: add support for sub-menus
MenuItemInfo *mii = malloc(sizeof(MenuItemInfo));
mii->menu_id = menu_id;
mii->title = title;
diff --git a/systray_linux.go b/systray_linux.go
new file mode 100644
index 0000000..1f508c7
--- /dev/null
+++ b/systray_linux.go
@@ -0,0 +1,29 @@
+package systray
+
+/*
+#cgo darwin CFLAGS: -DDARWIN -x objective-c -fobjc-arc
+#cgo darwin LDFLAGS: -framework Cocoa -framework WebKit
+
+#include "systray.h"
+*/
+import "C"
+
+// SetTemplateIcon sets the systray icon as a template icon (on macOS), falling back
+// to a regular icon on other platforms.
+// templateIconBytes and iconBytes should be the content of .ico for windows and
+// .ico/.jpg/.png for other platforms.
+func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) {
+ SetIcon(regularIconBytes)
+}
+
+// SetIcon sets the icon of a menu item. Only works on macOS and Windows.
+// iconBytes should be the content of .ico/.jpg/.png
+func (item *MenuItem) SetIcon(iconBytes []byte) {
+}
+
+// SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows, it
+// falls back to the regular icon bytes and on Linux it does nothing.
+// templateIconBytes and regularIconBytes should be the content of .ico for windows and
+// .ico/.jpg/.png for other platforms.
+func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) {
+}
diff --git a/systray_nonwindows.go b/systray_nonwindows.go
index 4868b55..6e06c3d 100644
--- a/systray_nonwindows.go
+++ b/systray_nonwindows.go
@@ -15,6 +15,10 @@ import (
"unsafe"
)
+func registerSystray() {
+ C.registerSystray()
+}
+
func nativeLoop() {
C.nativeLoop()
}
@@ -28,7 +32,7 @@ func quit() {
// for other platforms.
func SetIcon(iconBytes []byte) {
cstr := (*C.char)(unsafe.Pointer(&iconBytes[0]))
- C.setIcon(cstr, (C.int)(len(iconBytes)))
+ C.setIcon(cstr, (C.int)(len(iconBytes)), false)
}
// SetTitle sets the systray title, only available on Mac.
@@ -51,8 +55,13 @@ func addOrUpdateMenuItem(item *MenuItem) {
if item.checked {
checked = 1
}
+ var parentID int32 = 0
+ if item.parent != nil {
+ parentID = item.parent.id
+ }
C.add_or_update_menu_item(
C.int(item.id),
+ C.int(parentID),
C.CString(item.title),
C.CString(item.tooltip),
disabled,
@@ -60,13 +69,6 @@ func addOrUpdateMenuItem(item *MenuItem) {
)
}
-// SetIcon sets the icon of a menu item. Only available on Mac.
-// iconBytes should be the content of .ico/.jpg/.png
-func (item *MenuItem) SetIcon(iconBytes []byte) {
- cstr := (*C.char)(unsafe.Pointer(&iconBytes[0]))
- C.setMenuItemIcon(cstr, (C.int)(len(iconBytes)), C.int(item.id))
-}
-
func addSeparator(id int32) {
C.add_separator(C.int(id))
}
diff --git a/systray_windows.go b/systray_windows.go
index 1cb453c..02b5e6d 100644
--- a/systray_windows.go
+++ b/systray_windows.go
@@ -18,28 +18,41 @@ import (
// Helpful sources: https://github.com/golang/exp/blob/master/shiny/driver/internal/win32
var (
- k32 = windows.NewLazySystemDLL("Kernel32.dll")
- s32 = windows.NewLazySystemDLL("Shell32.dll")
+ g32 = windows.NewLazySystemDLL("Gdi32.dll")
+ pCreateCompatibleBitmap = g32.NewProc("CreateCompatibleBitmap")
+ pCreateCompatibleDC = g32.NewProc("CreateCompatibleDC")
+ pDeleteDC = g32.NewProc("DeleteDC")
+ pSelectObject = g32.NewProc("SelectObject")
+
+ k32 = windows.NewLazySystemDLL("Kernel32.dll")
+ pGetModuleHandle = k32.NewProc("GetModuleHandleW")
+
+ s32 = windows.NewLazySystemDLL("Shell32.dll")
+ pShellNotifyIcon = s32.NewProc("Shell_NotifyIconW")
+
u32 = windows.NewLazySystemDLL("User32.dll")
- pGetModuleHandle = k32.NewProc("GetModuleHandleW")
- pShellNotifyIcon = s32.NewProc("Shell_NotifyIconW")
+ pCreateMenu = u32.NewProc("CreateMenu")
pCreatePopupMenu = u32.NewProc("CreatePopupMenu")
pCreateWindowEx = u32.NewProc("CreateWindowExW")
pDefWindowProc = u32.NewProc("DefWindowProcW")
pDeleteMenu = u32.NewProc("DeleteMenu")
pDestroyWindow = u32.NewProc("DestroyWindow")
pDispatchMessage = u32.NewProc("DispatchMessageW")
+ pDrawIconEx = u32.NewProc("DrawIconEx")
pGetCursorPos = u32.NewProc("GetCursorPos")
+ pGetDC = u32.NewProc("GetDC")
pGetMenuItemID = u32.NewProc("GetMenuItemID")
pGetMessage = u32.NewProc("GetMessageW")
+ pGetSystemMetrics = u32.NewProc("GetSystemMetrics")
pInsertMenuItem = u32.NewProc("InsertMenuItemW")
+ pLoadCursor = u32.NewProc("LoadCursorW")
pLoadIcon = u32.NewProc("LoadIconW")
pLoadImage = u32.NewProc("LoadImageW")
- pLoadCursor = u32.NewProc("LoadCursorW")
pPostMessage = u32.NewProc("PostMessageW")
pPostQuitMessage = u32.NewProc("PostQuitMessage")
pRegisterClass = u32.NewProc("RegisterClassExW")
pRegisterWindowMessage = u32.NewProc("RegisterWindowMessageW")
+ pReleaseDC = u32.NewProc("ReleaseDC")
pSetForegroundWindow = u32.NewProc("SetForegroundWindow")
pSetMenuInfo = u32.NewProc("SetMenuInfo")
pSetMenuItemInfo = u32.NewProc("SetMenuItemInfoW")
@@ -150,7 +163,7 @@ type menuItemInfo struct {
ItemData uintptr
TypeData *uint16
Cch uint32
- Item windows.Handle
+ BMPItem windows.Handle
}
// The POINT structure defines the x- and y- coordinates of a point.
@@ -164,48 +177,35 @@ type winTray struct {
instance,
icon,
cursor,
- window,
- menu windows.Handle
+ window windows.Handle
loadedImages map[string]windows.Handle
- nid *notifyIconData
- wcex *wndClassEx
+ // menus keeps track of the submenus keyed by the menu item ID, plus 0
+ // which corresponds to the main popup menu.
+ menus map[uint32]windows.Handle
+ // menuOf keeps track of the menu each menu item belongs to.
+ menuOf map[uint32]windows.Handle
+ // menuItemIcons maintains the bitmap of each menu item (if applies). It's
+ // needed to show the icon correctly when showing a previously hidden menu
+ // item again.
+ menuItemIcons map[uint32]windows.Handle
+ visibleItems map[uint32][]uint32
+
+ nid *notifyIconData
+ wcex *wndClassEx
wmSystrayMessage,
wmTaskbarCreated uint32
-
- visibleItems []uint32
}
// Loads an image from file and shows it in tray.
-// LoadImage: https://msdn.microsoft.com/en-us/library/windows/desktop/ms648045(v=vs.85).aspx
// Shell_NotifyIcon: https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159(v=vs.85).aspx
func (t *winTray) setIcon(src string) error {
- const IMAGE_ICON = 1 // Loads an icon
- const LR_LOADFROMFILE = 0x00000010 // Loads the stand-alone image from the file
- const LR_DEFAULTSIZE = 0x00000040 // Loads default-size icon for windows(SM_CXICON x SM_CYICON) if cx, cy are set to zero
const NIF_ICON = 0x00000002
- // Save and reuse handles of loaded images
- h, ok := t.loadedImages[src]
- if !ok {
- srcPtr, err := windows.UTF16PtrFromString(src)
- if err != nil {
- return err
- }
- res, _, err := pLoadImage.Call(
- 0,
- uintptr(unsafe.Pointer(srcPtr)),
- IMAGE_ICON,
- 0,
- 0,
- LR_LOADFROMFILE|LR_DEFAULTSIZE,
- )
- if res == 0 {
- return err
- }
- h = windows.Handle(res)
- t.loadedImages[src] = h
+ h, err := t.loadIconFrom(src)
+ if err != nil {
+ return err
}
t.nid.Icon = h
@@ -236,18 +236,26 @@ var wt winTray
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms633573(v=vs.85).aspx
func (t *winTray) wndProc(hWnd windows.Handle, message uint32, wParam, lParam uintptr) (lResult uintptr) {
const (
- WM_COMMAND = 0x0111
- WM_DESTROY = 0x0002
- WM_ENDSESSION = 0x16
WM_RBUTTONUP = 0x0205
WM_LBUTTONUP = 0x0202
+ WM_COMMAND = 0x0111
+ WM_ENDSESSION = 0x0016
+ WM_CLOSE = 0x0010
+ WM_DESTROY = 0x0002
+ WM_CREATE = 0x0001
)
switch message {
+ case WM_CREATE:
+ systrayReady()
case WM_COMMAND:
- menuId := int32(wParam)
- if menuId != -1 {
- systrayMenuItemSelected(menuId)
+ menuItemId := int32(wParam)
+ // https://docs.microsoft.com/en-us/windows/win32/menurc/wm-command#menus
+ if menuItemId != -1 {
+ systrayMenuItemSelected(menuItemId)
}
+ case WM_CLOSE:
+ pDestroyWindow.Call(uintptr(t.window))
+ t.wcex.unregister()
case WM_DESTROY:
// same as WM_ENDSESSION, but throws 0 exit code after all
defer pPostQuitMessage.Call(uintptr(int32(0)))
@@ -310,6 +318,10 @@ func (t *winTray) initInstance() error {
)
t.wmSystrayMessage = WM_USER + 1
+ t.visibleItems = make(map[uint32][]uint32)
+ t.menus = make(map[uint32]windows.Handle)
+ t.menuOf = make(map[uint32]windows.Handle)
+ t.menuItemIcons = make(map[uint32]windows.Handle)
taskbarEventNamePtr, _ := windows.UTF16PtrFromString("TaskbarCreated")
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms644947
@@ -410,7 +422,7 @@ func (t *winTray) createMenu() error {
if menuHandle == 0 {
return err
}
- t.menu = windows.Handle(menuHandle)
+ t.menus[0] = windows.Handle(menuHandle)
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms647575(v=vs.85).aspx
mi := struct {
@@ -424,7 +436,7 @@ func (t *winTray) createMenu() error {
mi.Size = uint32(unsafe.Sizeof(mi))
res, _, err := pSetMenuInfo.Call(
- uintptr(t.menu),
+ uintptr(t.menus[0]),
uintptr(unsafe.Pointer(&mi)),
)
if res == 0 {
@@ -433,13 +445,39 @@ func (t *winTray) createMenu() error {
return nil
}
-func (t *winTray) addOrUpdateMenuItem(menuId int32, title string, disabled, checked bool) error {
+func (t *winTray) convertToSubMenu(menuItemId uint32) (windows.Handle, error) {
+ const MIIM_SUBMENU = 0x00000004
+
+ res, _, err := pCreateMenu.Call()
+ if res == 0 {
+ return 0, err
+ }
+ menu := windows.Handle(res)
+
+ mi := menuItemInfo{Mask: MIIM_SUBMENU, SubMenu: menu}
+ mi.Size = uint32(unsafe.Sizeof(mi))
+ res, _, err = pSetMenuItemInfo.Call(
+ uintptr(t.menuOf[menuItemId]),
+ uintptr(menuItemId),
+ 0,
+ uintptr(unsafe.Pointer(&mi)),
+ )
+ if res == 0 {
+ return 0, err
+ }
+ t.menus[menuItemId] = menu
+ return menu, nil
+}
+
+func (t *winTray) addOrUpdateMenuItem(menuItemId uint32, parentId uint32, title string, disabled, checked bool) error {
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx
const (
- MIIM_FTYPE = 0x00000100
- MIIM_STRING = 0x00000040
- MIIM_ID = 0x00000002
- MIIM_STATE = 0x00000001
+ MIIM_FTYPE = 0x00000100
+ MIIM_BITMAP = 0x00000080
+ MIIM_STRING = 0x00000040
+ MIIM_SUBMENU = 0x00000004
+ MIIM_ID = 0x00000002
+ MIIM_STATE = 0x00000001
)
const MFT_STRING = 0x00000000
const (
@@ -454,45 +492,61 @@ func (t *winTray) addOrUpdateMenuItem(menuId int32, title string, disabled, chec
mi := menuItemInfo{
Mask: MIIM_FTYPE | MIIM_STRING | MIIM_ID | MIIM_STATE,
Type: MFT_STRING,
- ID: uint32(menuId),
+ ID: uint32(menuItemId),
TypeData: titlePtr,
Cch: uint32(len(title)),
}
+ mi.Size = uint32(unsafe.Sizeof(mi))
if disabled {
mi.State |= MFS_DISABLED
}
if checked {
mi.State |= MFS_CHECKED
}
- mi.Size = uint32(unsafe.Sizeof(mi))
+ hIcon := t.menuItemIcons[menuItemId]
+ if hIcon > 0 {
+ mi.Mask |= MIIM_BITMAP
+ mi.BMPItem = hIcon
+ }
- // We set the menu item info based on the menuID
- res, _, err := pSetMenuItemInfo.Call(
- uintptr(t.menu),
- uintptr(menuId),
- 0,
- uintptr(unsafe.Pointer(&mi)),
- )
+ var res uintptr
+ menu, exists := t.menus[parentId]
+ if !exists {
+ menu, err = t.convertToSubMenu(parentId)
+ if err != nil {
+ return err
+ }
+ t.menus[parentId] = menu
+ } else if t.getVisibleItemIndex(parentId, menuItemId) != -1 {
+ // We set the menu item info based on the menuID
+ res, _, err = pSetMenuItemInfo.Call(
+ uintptr(menu),
+ uintptr(menuItemId),
+ 0,
+ uintptr(unsafe.Pointer(&mi)),
+ )
+ }
if res == 0 {
- t.addToVisibleItems(menuId)
- position := t.getVisibleItemIndex(menuId)
+ t.addToVisibleItems(parentId, menuItemId)
+ position := t.getVisibleItemIndex(parentId, menuItemId)
res, _, err = pInsertMenuItem.Call(
- uintptr(t.menu),
+ uintptr(menu),
uintptr(position),
1,
uintptr(unsafe.Pointer(&mi)),
)
if res == 0 {
- t.delFromVisibleItems(menuId)
+ t.delFromVisibleItems(parentId, menuItemId)
return err
}
+ t.menuOf[menuItemId] = menu
}
return nil
}
-func (t *winTray) addSeparatorMenuItem(menuId int32) error {
+func (t *winTray) addSeparatorMenuItem(menuItemId, parentId uint32) error {
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx
const (
MIIM_FTYPE = 0x00000100
@@ -504,15 +558,16 @@ func (t *winTray) addSeparatorMenuItem(menuId int32) error {
mi := menuItemInfo{
Mask: MIIM_FTYPE | MIIM_ID | MIIM_STATE,
Type: MFT_SEPARATOR,
- ID: uint32(menuId),
+ ID: uint32(menuItemId),
}
mi.Size = uint32(unsafe.Sizeof(mi))
- t.addToVisibleItems(menuId)
- position := t.getVisibleItemIndex(menuId)
+ t.addToVisibleItems(parentId, menuItemId)
+ position := t.getVisibleItemIndex(parentId, menuItemId)
+ menu := uintptr(t.menus[parentId])
res, _, err := pInsertMenuItem.Call(
- uintptr(t.menu),
+ menu,
uintptr(position),
1,
uintptr(unsafe.Pointer(&mi)),
@@ -524,20 +579,21 @@ func (t *winTray) addSeparatorMenuItem(menuId int32) error {
return nil
}
-func (t *winTray) hideMenuItem(menuId int32) error {
+func (t *winTray) hideMenuItem(menuItemId, parentId uint32) error {
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms647629(v=vs.85).aspx
const MF_BYCOMMAND = 0x00000000
const ERROR_SUCCESS syscall.Errno = 0
+ menu := uintptr(t.menus[parentId])
res, _, err := pDeleteMenu.Call(
- uintptr(t.menu),
- uintptr(uint32(menuId)),
+ menu,
+ uintptr(menuItemId),
MF_BYCOMMAND,
)
if res == 0 && err.(syscall.Errno) != ERROR_SUCCESS {
return err
}
- t.delFromVisibleItems(menuId)
+ t.delFromVisibleItems(parentId, menuItemId)
return nil
}
@@ -555,7 +611,7 @@ func (t *winTray) showMenu() error {
pSetForegroundWindow.Call(uintptr(t.window))
res, _, err = pTrackPopupMenu.Call(
- uintptr(t.menu),
+ uintptr(t.menus[0]),
TPM_BOTTOMALIGN|TPM_LEFTALIGN,
uintptr(p.X),
uintptr(p.Y),
@@ -570,31 +626,96 @@ func (t *winTray) showMenu() error {
return nil
}
-func (t *winTray) delFromVisibleItems(val int32) {
- for i, itemval := range t.visibleItems {
- if uint32(val) == itemval {
- t.visibleItems = append(t.visibleItems[:i], t.visibleItems[i+1:]...)
+func (t *winTray) delFromVisibleItems(parent, val uint32) {
+ visibleItems := t.visibleItems[parent]
+ for i, itemval := range visibleItems {
+ if val == itemval {
+ visibleItems = append(visibleItems[:i], visibleItems[i+1:]...)
break
}
}
}
-func (t *winTray) addToVisibleItems(val int32) {
- newvisible := append(t.visibleItems, uint32(val))
- sort.Slice(newvisible, func(i, j int) bool { return newvisible[i] < newvisible[j] })
- t.visibleItems = newvisible
+func (t *winTray) addToVisibleItems(parent, val uint32) {
+ if visibleItems, exists := t.visibleItems[parent]; !exists {
+ t.visibleItems[parent] = []uint32{val}
+ } else {
+ newvisible := append(visibleItems, val)
+ sort.Slice(newvisible, func(i, j int) bool { return newvisible[i] < newvisible[j] })
+ t.visibleItems[parent] = newvisible
+ }
}
-func (t *winTray) getVisibleItemIndex(val int32) int {
- for i, itemval := range t.visibleItems {
- if uint32(val) == itemval {
+func (t *winTray) getVisibleItemIndex(parent, val uint32) int {
+ for i, itemval := range t.visibleItems[parent] {
+ if val == itemval {
return i
}
}
return -1
}
-func nativeLoop() {
+// Loads an image from file to be shown in tray or menu item.
+// LoadImage: https://msdn.microsoft.com/en-us/library/windows/desktop/ms648045(v=vs.85).aspx
+func (t *winTray) loadIconFrom(src string) (windows.Handle, error) {
+ const IMAGE_ICON = 1 // Loads an icon
+ const LR_LOADFROMFILE = 0x00000010 // Loads the stand-alone image from the file
+ const LR_DEFAULTSIZE = 0x00000040 // Loads default-size icon for windows(SM_CXICON x SM_CYICON) if cx, cy are set to zero
+
+ // Save and reuse handles of loaded images
+ h, ok := t.loadedImages[src]
+ if !ok {
+ srcPtr, err := windows.UTF16PtrFromString(src)
+ if err != nil {
+ return 0, err
+ }
+ res, _, err := pLoadImage.Call(
+ 0,
+ uintptr(unsafe.Pointer(srcPtr)),
+ IMAGE_ICON,
+ 0,
+ 0,
+ LR_LOADFROMFILE|LR_DEFAULTSIZE,
+ )
+ if res == 0 {
+ return 0, err
+ }
+ h = windows.Handle(res)
+ t.loadedImages[src] = h
+ }
+ return h, nil
+}
+
+func (t *winTray) iconToBitmap(hIcon windows.Handle) (windows.Handle, error) {
+ const SM_CXSMICON = 49
+ const SM_CYSMICON = 50
+ const DI_NORMAL = 0x3
+ hDC, _, err := pGetDC.Call(uintptr(0))
+ if hDC == 0 {
+ return 0, err
+ }
+ defer pReleaseDC.Call(uintptr(0), hDC)
+ hMemDC, _, err := pCreateCompatibleDC.Call(hDC)
+ if hMemDC == 0 {
+ return 0, err
+ }
+ defer pDeleteDC.Call(hMemDC)
+ cx, _, _ := pGetSystemMetrics.Call(SM_CXSMICON)
+ cy, _, _ := pGetSystemMetrics.Call(SM_CYSMICON)
+ hMemBmp, _, err := pCreateCompatibleBitmap.Call(hDC, cx, cy)
+ if hMemBmp == 0 {
+ return 0, err
+ }
+ hOriginalBmp, _, _ := pSelectObject.Call(hMemDC, hMemBmp)
+ defer pSelectObject.Call(hMemDC, hOriginalBmp)
+ res, _, err := pDrawIconEx.Call(hMemDC, 0, 0, uintptr(hIcon), cx, cy, 0, uintptr(0), DI_NORMAL)
+ if res == 0 {
+ return 0, err
+ }
+ return windows.Handle(hMemBmp), nil
+}
+
+func registerSystray() {
if err := wt.initInstance(); err != nil {
log.Errorf("Unable to init instance: %v", err)
return
@@ -605,13 +726,9 @@ func nativeLoop() {
return
}
- defer func() {
- pDestroyWindow.Call(uintptr(wt.window))
- wt.wcex.unregister()
- }()
-
- go systrayReady()
+}
+func nativeLoop() {
// Main message pump.
m := &struct {
WindowHandle windows.Handle
@@ -652,35 +769,81 @@ func quit() {
)
}
-// SetIcon sets the systray icon.
-// iconBytes should be the content of .ico for windows and .ico/.jpg/.png
-// for other platforms.
-func SetIcon(iconBytes []byte) {
+func iconBytesToFilePath(iconBytes []byte) (string, error) {
bh := md5.Sum(iconBytes)
dataHash := hex.EncodeToString(bh[:])
iconFilePath := filepath.Join(os.TempDir(), "systray_temp_icon_"+dataHash)
if _, err := os.Stat(iconFilePath); os.IsNotExist(err) {
if err := ioutil.WriteFile(iconFilePath, iconBytes, 0644); err != nil {
- log.Errorf("Unable to write icon data to temp file: %v", err)
- return
+ return "", err
}
}
+ return iconFilePath, nil
+}
+// SetIcon sets the systray icon.
+// iconBytes should be the content of .ico for windows and .ico/.jpg/.png
+// for other platforms.
+func SetIcon(iconBytes []byte) {
+ iconFilePath, err := iconBytesToFilePath(iconBytes)
+ if err != nil {
+ log.Errorf("Unable to write icon data to temp file: %v", err)
+ return
+ }
if err := wt.setIcon(iconFilePath); err != nil {
log.Errorf("Unable to set icon: %v", err)
return
}
}
+// SetTemplateIcon sets the systray icon as a template icon (on macOS), falling back
+// to a regular icon on other platforms.
+// templateIconBytes and iconBytes should be the content of .ico for windows and
+// .ico/.jpg/.png for other platforms.
+func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) {
+ SetIcon(regularIconBytes)
+}
+
// SetTitle sets the systray title, only available on Mac.
func SetTitle(title string) {
// do nothing
}
-// SetIcon sets the icon of a menu item. Only available on Mac.
+func (item *MenuItem) parentId() uint32 {
+ if item.parent != nil {
+ return uint32(item.parent.id)
+ }
+ return 0
+}
+
+// SetIcon sets the icon of a menu item. Only works on macOS and Windows.
+// iconBytes should be the content of .ico/.jpg/.png
func (item *MenuItem) SetIcon(iconBytes []byte) {
- // do nothing
+ iconFilePath, err := iconBytesToFilePath(iconBytes)
+ if err != nil {
+ log.Errorf("Unable to write icon data to temp file: %v", err)
+ return
+ }
+
+ h, err := wt.loadIconFrom(iconFilePath)
+ if err != nil {
+ log.Errorf("Unable to load icon from temp file: %v", err)
+ return
+ }
+
+ h, err = wt.iconToBitmap(h)
+ if err != nil {
+ log.Errorf("Unable to convert icon to bitmap: %v", err)
+ return
+ }
+ wt.menuItemIcons[uint32(item.id)] = h
+
+ err = wt.addOrUpdateMenuItem(uint32(item.id), item.parentId(), item.title, item.disabled, item.checked)
+ if err != nil {
+ log.Errorf("Unable to addOrUpdateMenuItem: %v", err)
+ return
+ }
}
// SetTooltip sets the systray tooltip to display on mouse hover of the tray icon,
@@ -693,15 +856,23 @@ func SetTooltip(tooltip string) {
}
func addOrUpdateMenuItem(item *MenuItem) {
- err := wt.addOrUpdateMenuItem(item.id, item.title, item.disabled, item.checked)
+ err := wt.addOrUpdateMenuItem(uint32(item.id), item.parentId(), item.title, item.disabled, item.checked)
if err != nil {
log.Errorf("Unable to addOrUpdateMenuItem: %v", err)
return
}
}
+// SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows, it
+// falls back to the regular icon bytes and on Linux it does nothing.
+// templateIconBytes and regularIconBytes should be the content of .ico for windows and
+// .ico/.jpg/.png for other platforms.
+func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) {
+ item.SetIcon(regularIconBytes)
+}
+
func addSeparator(id int32) {
- err := wt.addSeparatorMenuItem(id)
+ err := wt.addSeparatorMenuItem(uint32(id), 0)
if err != nil {
log.Errorf("Unable to addSeparator: %v", err)
return
@@ -709,7 +880,7 @@ func addSeparator(id int32) {
}
func hideMenuItem(item *MenuItem) {
- err := wt.hideMenuItem(item.id)
+ err := wt.hideMenuItem(uint32(item.id), item.parentId())
if err != nil {
log.Errorf("Unable to hideMenuItem: %v", err)
return
diff --git a/webview_example/example.manifest b/webview_example/example.manifest
new file mode 100644
index 0000000..bf75b80
--- /dev/null
+++ b/webview_example/example.manifest
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ PerMonitorV2, PerMonitor
+ True
+
+
+
\ No newline at end of file
diff --git a/webview_example/main.go b/webview_example/main.go
new file mode 100644
index 0000000..160a8a5
--- /dev/null
+++ b/webview_example/main.go
@@ -0,0 +1,32 @@
+package main
+
+import (
+ "github.com/getlantern/systray"
+ "github.com/getlantern/systray/example/icon"
+)
+
+func main() {
+ systray.Register(onReady, nil)
+ configureWebview("Webview example", 1024, 768)
+}
+
+func onReady() {
+ systray.SetTemplateIcon(icon.Data, icon.Data)
+ systray.SetTitle("Webview example")
+ mShowLantern := systray.AddMenuItem("Show Lantern", "")
+ mShowWikipedia := systray.AddMenuItem("Show Wikipedia", "")
+ mQuit := systray.AddMenuItem("Quit", "Quit the whole app")
+ go func() {
+ for {
+ select {
+ case <-mShowLantern.ClickedCh:
+ showWebview("https://www.getlantern.org")
+ case <-mShowWikipedia.ClickedCh:
+ showWebview("https://www.wikipedia.org")
+ case <-mQuit.ClickedCh:
+ systray.Quit()
+ }
+ }
+ }()
+
+}
diff --git a/webview_example/rsrc.syso b/webview_example/rsrc.syso
new file mode 100644
index 0000000..dbad196
Binary files /dev/null and b/webview_example/rsrc.syso differ
diff --git a/webview_example/webview.h b/webview_example/webview.h
new file mode 100644
index 0000000..2e39c6a
--- /dev/null
+++ b/webview_example/webview.h
@@ -0,0 +1,3 @@
+void configureAppWindow(char* title, int width, int height);
+void showAppWindow(char* url);
+
diff --git a/webview_example/webview_darwin.m b/webview_example/webview_darwin.m
new file mode 100644
index 0000000..962f653
--- /dev/null
+++ b/webview_example/webview_darwin.m
@@ -0,0 +1,78 @@
+#import
+#import
+#include "webview.h"
+
+NSWindowController *windowController = nil;
+NSWindow *window = nil;
+WKWebView *webView = nil;
+
+void configureAppWindow(char* title, int width, int height)
+{
+ if (windowController != nil) {
+ // already configured, ignore
+ return;
+ }
+
+ NSApplication *app = [NSApplication sharedApplication];
+ [app setActivationPolicy:NSApplicationActivationPolicyRegular];
+ [app activateIgnoringOtherApps:YES];
+
+ NSRect frame = NSMakeRect(0, 0, width, height);
+ int mask = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable | NSWindowStyleMaskClosable;
+ window = [[NSWindow alloc] initWithContentRect:frame
+ styleMask:mask
+ backing:NSBackingStoreBuffered
+ defer:NO];
+ [window setTitle:[[NSString alloc] initWithUTF8String:title]];
+ [window center];
+
+ NSView *contentView = [window contentView];
+ webView = [[WKWebView alloc] initWithFrame:[contentView bounds]];
+ [webView setTranslatesAutoresizingMaskIntoConstraints:NO];
+ [contentView addSubview:webView];
+ [contentView addConstraint:
+ [NSLayoutConstraint constraintWithItem:webView
+ attribute:NSLayoutAttributeWidth
+ relatedBy:NSLayoutRelationEqual
+ toItem:contentView
+ attribute:NSLayoutAttributeWidth
+ multiplier:1
+ constant:0]];
+ [contentView addConstraint:
+ [NSLayoutConstraint constraintWithItem:webView
+ attribute:NSLayoutAttributeHeight
+ relatedBy:NSLayoutRelationEqual
+ toItem:contentView
+ attribute:NSLayoutAttributeHeight
+ multiplier:1
+ constant:0]];
+
+ // Window controller:
+ windowController = [[NSWindowController alloc] initWithWindow:window];
+
+ free(title);
+ [NSApp run];
+}
+
+void doShowAppWindow(char* url)
+{
+ if (windowController == nil) {
+ // no app window to open
+ return;
+ }
+
+ id nsURL = [NSURL URLWithString:[[NSString alloc] initWithUTF8String:url]];
+ id req = [[NSURLRequest alloc] initWithURL: nsURL
+ cachePolicy: NSURLRequestUseProtocolCachePolicy
+ timeoutInterval: 5];
+ [webView loadRequest:req];
+ [windowController showWindow:window];
+ free(url);
+}
+
+void showAppWindow(char* url)
+{
+ dispatch_async(dispatch_get_main_queue(), ^{
+ doShowAppWindow(url);
+ });
+}
diff --git a/webview_example/webview_linux.c b/webview_example/webview_linux.c
new file mode 100644
index 0000000..6983fdb
--- /dev/null
+++ b/webview_example/webview_linux.c
@@ -0,0 +1,60 @@
+#include
+#include
+#include
+#include "webview.h"
+
+static GtkWindow *web_window = NULL;
+static WebKitWebView *web_view = NULL;
+
+static gint x, y;
+static bool needsMove = false;
+
+gboolean on_window_deleted(GtkWidget *window, GdkEvent *event, gpointer data)
+{
+ gtk_window_get_position(GTK_WINDOW(window), &x, &y);
+ needsMove = true;
+ gtk_widget_hide(window);
+ return TRUE;
+}
+
+void configureAppWindow(char* title, int width, int height)
+{
+ // Create an 800x600 window that will contain the browser instance
+ web_window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
+ gtk_window_set_title(web_window, title);
+ gtk_window_set_default_size(web_window, width, height);
+ gtk_window_set_skip_taskbar_hint (web_window, TRUE);
+ g_signal_connect(G_OBJECT(web_window), "delete-event", G_CALLBACK(on_window_deleted), NULL);
+
+ // Create a browser instance
+ web_view = WEBKIT_WEB_VIEW(webkit_web_view_new());
+
+ // Put the browser area into the web window
+ gtk_container_add(GTK_CONTAINER(web_window), GTK_WIDGET(web_view));
+
+ // Make sure that when the browser area becomes visible, it will get mouse
+ // and keyboard events
+ gtk_widget_grab_focus(GTK_WIDGET(web_view));
+ free(title);
+ gtk_main();
+}
+
+gboolean do_show_app_window(gpointer data)
+{
+ gtk_widget_show_all(GTK_WIDGET(web_window));
+ if (needsMove) {
+ gtk_window_move(web_window, x, y);
+ needsMove = false;
+ }
+ gtk_window_present(web_window);
+ return FALSE;
+}
+
+void showAppWindow(char* url)
+{
+ // Load a web page into the browser instance
+ webkit_web_view_load_uri(web_view, url);
+
+ g_idle_add(do_show_app_window, NULL);
+ free(url);
+}
diff --git a/webview_example/webview_nonwindows.go b/webview_example/webview_nonwindows.go
new file mode 100644
index 0000000..bd176cc
--- /dev/null
+++ b/webview_example/webview_nonwindows.go
@@ -0,0 +1,20 @@
+//+build !windows
+
+package main
+
+/*
+#cgo linux pkg-config: webkit2gtk-4.0
+#cgo darwin CFLAGS: -DDARWIN -x objective-c -fobjc-arc
+#cgo darwin LDFLAGS: -framework Cocoa -framework Webkit
+
+#include "webview.h"
+*/
+import "C"
+
+func configureWebview(title string, width, height int) {
+ C.configureAppWindow(C.CString(title), C.int(width), C.int(height))
+}
+
+func showWebview(url string) {
+ C.showAppWindow(C.CString(url))
+}
diff --git a/webview_example/webview_windows.go b/webview_example/webview_windows.go
new file mode 100644
index 0000000..f116c48
--- /dev/null
+++ b/webview_example/webview_windows.go
@@ -0,0 +1,34 @@
+package main
+
+import (
+ "fmt"
+ "github.com/lxn/walk"
+)
+
+var webView *walk.WebView
+var mainWindow *walk.MainWindow
+var err error
+
+func configureWebview(title string, width, height int) {
+ mainWindow, err = walk.NewMainWindow()
+ if err != nil {
+ panic(fmt.Sprintf("Failed to create main window: %v\n", err))
+ }
+ mainWindow.SetTitle(title)
+ mainWindow.SetWidth(width)
+ mainWindow.SetHeight(height)
+ layout := walk.NewVBoxLayout()
+ if err := mainWindow.SetLayout(layout); err != nil {
+ panic(fmt.Sprintf("Failed to set layout: %v\n", err))
+ }
+ webView, err = walk.NewWebView(mainWindow)
+ if err != nil {
+ panic(fmt.Sprintf("Failed to create webview window: %v\n", err))
+ }
+ mainWindow.SetVisible(false)
+ mainWindow.Run()
+}
+func showWebview(url string) {
+ mainWindow.SetVisible(true)
+ webView.SetURL(url)
+}