From 5fa58247084fcfc152725a2a2fc4beae7d8c5fe1 Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Wed, 18 Sep 2019 06:58:35 -0500 Subject: [PATCH 01/11] Added support for App Window --- example/main.go | 6 +-- go.mod | 8 ++++ go.sum | 22 +++++++++++ systray.h | 2 + systray_browser_darwin.m | 79 ++++++++++++++++++++++++++++++++++++++++ systray_browser_linux.c | 46 +++++++++++++++++++++++ systray_darwin.m | 5 +++ systray_linux.c | 3 ++ systray_nonwindows.go | 16 +++++++- 9 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 systray_browser_darwin.m create mode 100644 systray_browser_linux.c diff --git a/example/main.go b/example/main.go index 5542b89..88bf7a5 100644 --- a/example/main.go +++ b/example/main.go @@ -7,7 +7,6 @@ import ( "github.com/getlantern/systray" "github.com/getlantern/systray/example/icon" - "github.com/skratchdot/open-golang/open" ) func main() { @@ -22,6 +21,7 @@ func main() { } func onReady() { + systray.EnableAppWindow("Lantern", 1024, 768) systray.SetIcon(icon.Data) systray.SetTitle("Awesome App") systray.SetTooltip("Lantern") @@ -42,7 +42,7 @@ func onReady() { mChecked := systray.AddMenuItem("Unchecked", "Check Me") mEnabled := systray.AddMenuItem("Enabled", "Enabled") systray.AddMenuItem("Ignored", "Ignored") - mUrl := systray.AddMenuItem("Open Lantern.org", "my home") + 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. @@ -67,7 +67,7 @@ func onReady() { mEnabled.SetTitle("Disabled") mEnabled.Disable() case <-mUrl.ClickedCh: - open.Run("https://www.getlantern.org") + systray.ShowAppWindow("https://www.getlantern.org") case <-mToggle.ClickedCh: if shown { mQuitOrig.Hide() diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..91cdf7b --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/getlantern/systray + +go 1.12 + +require ( + github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 + github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e1d551d --- /dev/null +++ b/go.sum @@ -0,0 +1,22 @@ +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/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e h1:VAzdS5Nw68fbf5RZ8RDVlUvPXNU6Z3jtPCK/qvm4FoQ= +github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/systray.h b/systray.h index 36bcf98..cecdc40 100644 --- a/systray.h +++ b/systray.h @@ -7,6 +7,8 @@ void setIcon(const char* iconBytes, int length); void setMenuItemIcon(const char* iconBytes, int length, int menuId); void setTitle(char* title); void setTooltip(char* tooltip); +void configureAppWindow(char* title, int width, int height); +void showAppWindow(char* url); void add_or_update_menu_item(int menuId, char* title, char* tooltip, short disabled, short checked); void add_separator(int menuId); void hide_menu_item(int menuId); diff --git a/systray_browser_darwin.m b/systray_browser_darwin.m new file mode 100644 index 0000000..c20c7f6 --- /dev/null +++ b/systray_browser_darwin.m @@ -0,0 +1,79 @@ +#import +#import +#include "systray.h" + +NSWindowController *windowController = nil; +WKWebView *webView = nil; + +void doConfigureAppWindow(char* title, int width, int height) +{ + if (windowController != nil) { + // already configured, ignore + return; + } + + NSRect frame = NSMakeRect(0, 0, width, height); + int mask = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable | NSWindowStyleMaskClosable; + id window = + [[NSWindow alloc] initWithContentRect:frame + styleMask:mask + backing:NSBackingStoreBuffered + defer:NO]; + [window setTitle:[[NSString alloc] initWithUTF8String:title]]; + [window makeKeyAndOrderFront:nil]; + [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); +} + +void configureAppWindow(char* title, int width, int height) +{ + dispatch_async(dispatch_get_main_queue(), ^{ + doConfigureAppWindow(title, width, height); + }); +} + +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.window orderFrontRegardless]; + free(url); +} + +void showAppWindow(char* url) +{ + dispatch_async(dispatch_get_main_queue(), ^{ + doShowAppWindow(url); + }); +} diff --git a/systray_browser_linux.c b/systray_browser_linux.c new file mode 100644 index 0000000..af32294 --- /dev/null +++ b/systray_browser_linux.c @@ -0,0 +1,46 @@ +#include +#include + +static GtkWidget *web_window = NULL; +static WebKitWebView *webView = NULL; + +static gboolean closeWebViewCb(WebKitWebView* webView, GtkWidget* window); + +void prepareBrowser() +{ + // Create an 800x600 window that will contain the browser instance + web_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_default_size(GTK_WINDOW(web_window), 800, 600); + + // Create a browser instance + webView = WEBKIT_WEB_VIEW(webkit_web_view_new()); + + // Put the browser area into the web window + gtk_container_add(GTK_CONTAINER(web_window), GTK_WIDGET(webView)); + + g_signal_connect(webView, "close", G_CALLBACK(closeWebViewCb), web_window); + + // Make sure that when the browser area becomes visible, it will get mouse + // and keyboard events + gtk_widget_grab_focus(GTK_WIDGET(webView)); +} + +gboolean do_open_in_browser(gpointer data) +{ + gtk_widget_show_all(web_window); + return TRUE; +} + +void openInBrowser(char* url) +{ + // Load a web page into the browser instance + webkit_web_view_load_uri(webView, url); + + gdk_threads_add_idle(do_open_in_browser, url); +} + +static gboolean closeWebViewCb(WebKitWebView* webView, GtkWidget* window) +{ + gtk_widget_destroy(window); + return TRUE; +} \ No newline at end of file diff --git a/systray_darwin.m b/systray_darwin.m index b0f2172..7189ef5 100644 --- a/systray_darwin.m +++ b/systray_darwin.m @@ -70,6 +70,11 @@ systray_ready(); } +- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender +{ + return FALSE; +} + - (void)applicationWillTerminate:(NSNotification *)aNotification { systray_on_exit(); diff --git a/systray_linux.c b/systray_linux.c index 72cd614..20eedaf 100644 --- a/systray_linux.c +++ b/systray_linux.c @@ -23,6 +23,8 @@ typedef struct { short checked; } MenuItemInfo; +void prepareBrowser(); + int nativeLoop(void) { gtk_init(0, NULL); global_app_indicator = app_indicator_new("systray", "", @@ -31,6 +33,7 @@ int nativeLoop(void) { global_tray_menu = gtk_menu_new(); app_indicator_set_menu(global_app_indicator, GTK_MENU(global_tray_menu)); systray_ready(); + prepareBrowser(); gtk_main(); systray_on_exit(); return 0; diff --git a/systray_nonwindows.go b/systray_nonwindows.go index 4868b55..03af1bb 100644 --- a/systray_nonwindows.go +++ b/systray_nonwindows.go @@ -3,9 +3,10 @@ package systray /* -#cgo linux pkg-config: gtk+-3.0 appindicator3-0.1 +#cgo linux CFLAGS: -DWEBVIEW_GTK=1 +#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 appindicator3-0.1 #cgo darwin CFLAGS: -DDARWIN -x objective-c -fobjc-arc -#cgo darwin LDFLAGS: -framework Cocoa +#cgo darwin LDFLAGS: -framework Cocoa -framework WebKit #include "systray.h" */ @@ -42,6 +43,17 @@ func SetTooltip(tooltip string) { C.setTooltip(C.CString(tooltip)) } +// EnableAppWindow enables a single application window for this app. +func EnableAppWindow(title string, width int, height int) { + C.configureAppWindow(C.CString(title), C.int(width), C.int(height)) +} + +// ShowAppWindow shows the given URL in the application window. Only works if +// configureAppWindow has been called first. +func ShowAppWindow(url string) { + C.showAppWindow(C.CString(url)) +} + func addOrUpdateMenuItem(item *MenuItem) { var disabled C.short if item.disabled { From a7d9f30ed6a272b2bc226dd4ff0af0dbf1764b52 Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Thu, 19 Sep 2019 12:33:15 -0500 Subject: [PATCH 02/11] Switched Windows to use lxn/walk --- example/example.manifest | 15 + example/main.go | 3 +- example/rsrc.syso | Bin 0 -> 1045 bytes go.mod | 5 + go.sum | 11 + systray.go | 10 +- systray_nonwindows.go | 22 +- systray_windows.go | 781 ++++++--------------------------------- systray_windows_test.go | 132 ------- 9 files changed, 170 insertions(+), 809 deletions(-) create mode 100644 example/example.manifest create mode 100644 example/rsrc.syso delete mode 100644 systray_windows_test.go diff --git a/example/example.manifest b/example/example.manifest new file mode 100644 index 0000000..bf75b80 --- /dev/null +++ b/example/example.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + PerMonitorV2, PerMonitor + True + + + \ No newline at end of file diff --git a/example/main.go b/example/main.go index 88bf7a5..5efccbd 100644 --- a/example/main.go +++ b/example/main.go @@ -17,11 +17,10 @@ func main() { fmt.Println("Finished onExit") } // Should be called at the very beginning of main(). - systray.Run(onReady, onExit) + systray.RunWithAppWindow("Lantern", 1024, 768, onReady, onExit) } func onReady() { - systray.EnableAppWindow("Lantern", 1024, 768) systray.SetIcon(icon.Data) systray.SetTitle("Awesome App") systray.SetTooltip("Lantern") diff --git a/example/rsrc.syso b/example/rsrc.syso new file mode 100644 index 0000000000000000000000000000000000000000..dbad19625abfeeea83ca5dc19af6828354f8e964 GIT binary patch literal 1045 zcmeZaWMlw=|I7>w5EcugUQuyTGDr}LcQP|D*f20Kd}W3RfYdoKFgQSXFp`l$f`I{q z8(<<3nh|WS0RsbuI427Og9ifxLj(f@Ln72{kRBWRirgH9vecsD%=|oCB||*}C56{=NoFR7CML0qBjeFt`|J`Ug5z$IXQw@EB0$jMAjEXmBz zBQo}kl@LK=Qx5THaB4|OW?p(RLI&)Pl!8ph^2DOlywu`inCTfMB?VUc`q1Q|2TvY) z$@#hZ!M>jQMh1pv`p^J`nX6)l~P)1{CK+fu*Y|YHT!oa`)0GzZXj{pDw literal 0 HcmV?d00001 diff --git a/go.mod b/go.mod index 91cdf7b..505047b 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,10 @@ go 1.12 require ( github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 + github.com/getlantern/uuid v1.2.0 + github.com/lxn/walk v0.0.0-20190919092010-4386a6d42094 + github.com/lxn/win v0.0.0-20190919090605-24c5960b03d8 // indirect github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e + golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13 // indirect + gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect ) diff --git a/go.sum b/go.sum index e1d551d..9e9e928 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,14 @@ github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0F 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/getlantern/uuid v1.2.0 h1:pGrGaCV7XEaG6lvjWkwf8Y92BjB/9yFmkKsNFpRQ7rc= +github.com/getlantern/uuid v1.2.0/go.mod h1:uX10hOzZUUDR+oYNSIks+RcozOEiwTNC/K2rw9SUi1k= 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-20190919092010-4386a6d42094 h1:axV9dR1/LfxQTFTpzjiBzQV9tFarx+Evs2ONi/ceq2U= +github.com/lxn/walk v0.0.0-20190919092010-4386a6d42094/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= +github.com/lxn/win v0.0.0-20190919090605-24c5960b03d8 h1:RVMGIuuNgrpGB7I79f6xfhGCkpN47IaEGh8VTM0p7Xc= +github.com/lxn/win v0.0.0-20190919090605-24c5960b03d8/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -20,3 +26,8 @@ github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e h1:VAzdS5Nw github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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-20190919044723-0c1ff786ef13 h1:/zi0zzlPHWXYXrO1LjNRByFu8sdGgCkj2JLDdBIB84k= +golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13/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 d433173..750e5f3 100644 --- a/systray.go +++ b/systray.go @@ -50,10 +50,14 @@ var ( ) // Run initializes GUI and starts the event loop, then invokes the onReady -// callback. -// It blocks until systray.Quit() is called. +// callback. It blocks until systray.Quit() is called. // Should be called at the very beginning of main() to lock at main thread. func Run(onReady func(), onExit func()) { + RunWithAppWindow("", 0, 0, onReady, onExit) +} + +// RunWithAppWindow is like Run but also enables an application window with the given title. +func RunWithAppWindow(title string, width int, height int, onReady func(), onExit func()) { runtime.LockOSThread() atomic.StoreInt64(&hasStarted, 1) @@ -78,7 +82,7 @@ func Run(onReady func(), onExit func()) { } systrayExit = onExit - nativeLoop() + nativeLoop(title, width, height) } // Quit the systray diff --git a/systray_nonwindows.go b/systray_nonwindows.go index 03af1bb..fdacfa2 100644 --- a/systray_nonwindows.go +++ b/systray_nonwindows.go @@ -24,6 +24,17 @@ func quit() { C.quit() } +// EnableAppWindow enables a single application window for this app. +func EnableAppWindow(title string, width int, height int) { + C.configureAppWindow(C.CString(title), C.int(width), C.int(height)) +} + +// ShowAppWindow shows the given URL in the application window. Only works if +// configureAppWindow has been called first. +func ShowAppWindow(url string) { + C.showAppWindow(C.CString(url)) +} + // SetIcon sets the systray icon. // iconBytes should be the content of .ico for windows and .ico/.jpg/.png // for other platforms. @@ -43,17 +54,6 @@ func SetTooltip(tooltip string) { C.setTooltip(C.CString(tooltip)) } -// EnableAppWindow enables a single application window for this app. -func EnableAppWindow(title string, width int, height int) { - C.configureAppWindow(C.CString(title), C.int(width), C.int(height)) -} - -// ShowAppWindow shows the given URL in the application window. Only works if -// configureAppWindow has been called first. -func ShowAppWindow(url string) { - C.showAppWindow(C.CString(url)) -} - func addOrUpdateMenuItem(item *MenuItem) { var disabled C.short if item.disabled { diff --git a/systray_windows.go b/systray_windows.go index 1cb453c..4f8e731 100644 --- a/systray_windows.go +++ b/systray_windows.go @@ -3,719 +3,178 @@ package systray import ( - "crypto/md5" - "encoding/hex" + "fmt" "io/ioutil" "os" - "path/filepath" - "sort" - "syscall" - "unsafe" + "sync/atomic" - "golang.org/x/sys/windows" + "github.com/lxn/walk" ) -// Helpful sources: https://github.com/golang/exp/blob/master/shiny/driver/internal/win32 - var ( - k32 = windows.NewLazySystemDLL("Kernel32.dll") - s32 = windows.NewLazySystemDLL("Shell32.dll") - u32 = windows.NewLazySystemDLL("User32.dll") - pGetModuleHandle = k32.NewProc("GetModuleHandleW") - pShellNotifyIcon = s32.NewProc("Shell_NotifyIconW") - pCreatePopupMenu = u32.NewProc("CreatePopupMenu") - pCreateWindowEx = u32.NewProc("CreateWindowExW") - pDefWindowProc = u32.NewProc("DefWindowProcW") - pDeleteMenu = u32.NewProc("DeleteMenu") - pDestroyWindow = u32.NewProc("DestroyWindow") - pDispatchMessage = u32.NewProc("DispatchMessageW") - pGetCursorPos = u32.NewProc("GetCursorPos") - pGetMenuItemID = u32.NewProc("GetMenuItemID") - pGetMessage = u32.NewProc("GetMessageW") - pInsertMenuItem = u32.NewProc("InsertMenuItemW") - 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") - pSetForegroundWindow = u32.NewProc("SetForegroundWindow") - pSetMenuInfo = u32.NewProc("SetMenuInfo") - pSetMenuItemInfo = u32.NewProc("SetMenuItemInfoW") - pShowWindow = u32.NewProc("ShowWindow") - pTrackPopupMenu = u32.NewProc("TrackPopupMenu") - pTranslateMessage = u32.NewProc("TranslateMessage") - pUnregisterClass = u32.NewProc("UnregisterClassW") - pUpdateWindow = u32.NewProc("UpdateWindow") + tmpDir string + mainWindow *walk.MainWindow + webView *walk.WebView + notifyIcon *walk.NotifyIcon + + actions = make(map[int32]*walk.Action) + nextActionId int32 + + okayToClose int32 ) -// Contains window class information. -// It is used with the RegisterClassEx and GetClassInfoEx functions. -// https://msdn.microsoft.com/en-us/library/ms633577.aspx -type wndClassEx struct { - Size, Style uint32 - WndProc uintptr - ClsExtra, WndExtra int32 - Instance, Icon, Cursor, Background windows.Handle - MenuName, ClassName *uint16 - IconSm windows.Handle -} - -// Registers a window class for subsequent use in calls to the CreateWindow or CreateWindowEx function. -// https://msdn.microsoft.com/en-us/library/ms633587.aspx -func (w *wndClassEx) register() error { - w.Size = uint32(unsafe.Sizeof(*w)) - res, _, err := pRegisterClass.Call(uintptr(unsafe.Pointer(w))) - if res == 0 { - return err +func nativeLoop(title string, width int, height int) { + var err error + mainWindow, err = walk.NewMainWindow() + if err != nil { + fail("Unable to create main window", err) } - return nil -} - -// Unregisters a window class, freeing the memory required for the class. -// https://msdn.microsoft.com/en-us/library/ms644899.aspx -func (w *wndClassEx) unregister() error { - res, _, err := pUnregisterClass.Call( - uintptr(unsafe.Pointer(w.ClassName)), - uintptr(w.Instance), - ) - if res == 0 { - return err + mainWindow.Closing().Attach(func(canceled *bool, reason walk.CloseReason) { + fmt.Println(reason) + // don't close app unless we're actually finished + actuallyClose := atomic.LoadInt32(&okayToClose) == 1 + *canceled = !actuallyClose + if !actuallyClose { + mainWindow.SetVisible(false) + } + }) + layout := walk.NewVBoxLayout() + if err := mainWindow.SetLayout(layout); err != nil { + fail("Unable to set main layout", err) } - return nil -} - -// Contains information that the system needs to display notifications in the notification area. -// Used by Shell_NotifyIcon. -// https://msdn.microsoft.com/en-us/library/windows/desktop/bb773352(v=vs.85).aspx -// https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159 -type notifyIconData struct { - Size uint32 - Wnd windows.Handle - ID, Flags, CallbackMessage uint32 - Icon windows.Handle - Tip [128]uint16 - State, StateMask uint32 - Info [256]uint16 - Timeout, Version uint32 - InfoTitle [64]uint16 - InfoFlags uint32 - GuidItem windows.GUID - BalloonIcon windows.Handle -} - -func (nid *notifyIconData) add() error { - const NIM_ADD = 0x00000000 - res, _, err := pShellNotifyIcon.Call( - uintptr(NIM_ADD), - uintptr(unsafe.Pointer(nid)), - ) - if res == 0 { - return err + notifyIcon, err = walk.NewNotifyIcon(mainWindow) + if err != nil { + fail("Unable to create notify icon", err) } - return nil -} - -func (nid *notifyIconData) modify() error { - const NIM_MODIFY = 0x00000001 - res, _, err := pShellNotifyIcon.Call( - uintptr(NIM_MODIFY), - uintptr(unsafe.Pointer(nid)), - ) - if res == 0 { - return err - } - return nil -} - -func (nid *notifyIconData) delete() error { - const NIM_DELETE = 0x00000002 - res, _, err := pShellNotifyIcon.Call( - uintptr(NIM_DELETE), - uintptr(unsafe.Pointer(nid)), - ) - if res == 0 { - return err - } - return nil -} - -// Contains information about a menu item. -// https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx -type menuItemInfo struct { - Size, Mask, Type, State uint32 - ID uint32 - SubMenu, Checked, Unchecked windows.Handle - ItemData uintptr - TypeData *uint16 - Cch uint32 - Item windows.Handle -} - -// The POINT structure defines the x- and y- coordinates of a point. -// https://msdn.microsoft.com/en-us/library/windows/desktop/dd162805(v=vs.85).aspx -type point struct { - X, Y int32 -} - -// Contains information about loaded resources -type winTray struct { - instance, - icon, - cursor, - window, - menu windows.Handle - - loadedImages map[string]windows.Handle - 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 title != "" { + webView, err = walk.NewWebView(mainWindow) if err != nil { - return err + fail("Unable to create web view", err) } - res, _, err := pLoadImage.Call( - 0, - uintptr(unsafe.Pointer(srcPtr)), - IMAGE_ICON, - 0, - 0, - LR_LOADFROMFILE|LR_DEFAULTSIZE, - ) - if res == 0 { - return err + if err := mainWindow.SetTitle(title); err != nil { + fail("Unable to set main title", err) } - h = windows.Handle(res) - t.loadedImages[src] = h - } - - t.nid.Icon = h - t.nid.Flags |= NIF_ICON - t.nid.Size = uint32(unsafe.Sizeof(*t.nid)) - - return t.nid.modify() -} - -// Sets tooltip on icon. -// Shell_NotifyIcon: https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159(v=vs.85).aspx -func (t *winTray) setTooltip(src string) error { - const NIF_TIP = 0x00000004 - b, err := windows.UTF16FromString(src) - if err != nil { - return err - } - copy(t.nid.Tip[:], b[:]) - t.nid.Flags |= NIF_TIP - t.nid.Size = uint32(unsafe.Sizeof(*t.nid)) - - return t.nid.modify() -} - -var wt winTray - -// WindowProc callback function that processes messages sent to a window. -// 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 - ) - switch message { - case WM_COMMAND: - menuId := int32(wParam) - if menuId != -1 { - systrayMenuItemSelected(menuId) + if err := mainWindow.SetWidth(width); err != nil { + fail("Unable to set width", err) } - case WM_DESTROY: - // same as WM_ENDSESSION, but throws 0 exit code after all - defer pPostQuitMessage.Call(uintptr(int32(0))) - fallthrough - case WM_ENDSESSION: - if t.nid != nil { - t.nid.delete() - } - systrayExit() - case t.wmSystrayMessage: - switch lParam { - case WM_RBUTTONUP, WM_LBUTTONUP: - t.showMenu() - } - case t.wmTaskbarCreated: // on explorer.exe restarts - t.nid.add() - default: - // Calls the default window procedure to provide default processing for any window messages that an application does not process. - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms633572(v=vs.85).aspx - lResult, _, _ = pDefWindowProc.Call( - uintptr(hWnd), - uintptr(message), - uintptr(wParam), - uintptr(lParam), - ) - } - return -} - -func (t *winTray) initInstance() error { - const IDI_APPLICATION = 32512 - const IDC_ARROW = 32512 // Standard arrow - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms633548(v=vs.85).aspx - const SW_HIDE = 0 - const CW_USEDEFAULT = 0x80000000 - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms632600(v=vs.85).aspx - const ( - WS_CAPTION = 0x00C00000 - WS_MAXIMIZEBOX = 0x00010000 - WS_MINIMIZEBOX = 0x00020000 - WS_OVERLAPPED = 0x00000000 - WS_SYSMENU = 0x00080000 - WS_THICKFRAME = 0x00040000 - - WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX - ) - // https://msdn.microsoft.com/en-us/library/windows/desktop/ff729176 - const ( - CS_HREDRAW = 0x0002 - CS_VREDRAW = 0x0001 - ) - const NIF_MESSAGE = 0x00000001 - - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644931(v=vs.85).aspx - const WM_USER = 0x0400 - - const ( - className = "SystrayClass" - windowName = "" - ) - - t.wmSystrayMessage = WM_USER + 1 - - taskbarEventNamePtr, _ := windows.UTF16PtrFromString("TaskbarCreated") - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644947 - res, _, err := pRegisterWindowMessage.Call( - uintptr(unsafe.Pointer(taskbarEventNamePtr)), - ) - t.wmTaskbarCreated = uint32(res) - - t.loadedImages = make(map[string]windows.Handle) - - instanceHandle, _, err := pGetModuleHandle.Call(0) - if instanceHandle == 0 { - return err - } - t.instance = windows.Handle(instanceHandle) - - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms648072(v=vs.85).aspx - iconHandle, _, err := pLoadIcon.Call(0, uintptr(IDI_APPLICATION)) - if iconHandle == 0 { - return err - } - t.icon = windows.Handle(iconHandle) - - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms648391(v=vs.85).aspx - cursorHandle, _, err := pLoadCursor.Call(0, uintptr(IDC_ARROW)) - if cursorHandle == 0 { - return err - } - t.cursor = windows.Handle(cursorHandle) - - classNamePtr, err := windows.UTF16PtrFromString(className) - if err != nil { - return err - } - - windowNamePtr, err := windows.UTF16PtrFromString(windowName) - if err != nil { - return err - } - - t.wcex = &wndClassEx{ - Style: CS_HREDRAW | CS_VREDRAW, - WndProc: windows.NewCallback(t.wndProc), - Instance: t.instance, - Icon: t.icon, - Cursor: t.cursor, - Background: windows.Handle(6), // (COLOR_WINDOW + 1) - ClassName: classNamePtr, - IconSm: t.icon, - } - if err := t.wcex.register(); err != nil { - return err - } - - windowHandle, _, err := pCreateWindowEx.Call( - uintptr(0), - uintptr(unsafe.Pointer(classNamePtr)), - uintptr(unsafe.Pointer(windowNamePtr)), - uintptr(WS_OVERLAPPEDWINDOW), - uintptr(CW_USEDEFAULT), - uintptr(CW_USEDEFAULT), - uintptr(CW_USEDEFAULT), - uintptr(CW_USEDEFAULT), - uintptr(0), - uintptr(0), - uintptr(t.instance), - uintptr(0), - ) - if windowHandle == 0 { - return err - } - t.window = windows.Handle(windowHandle) - - pShowWindow.Call( - uintptr(t.window), - uintptr(SW_HIDE), - ) - - pUpdateWindow.Call( - uintptr(t.window), - ) - - t.nid = ¬ifyIconData{ - Wnd: windows.Handle(t.window), - ID: 100, - Flags: NIF_MESSAGE, - CallbackMessage: t.wmSystrayMessage, - } - t.nid.Size = uint32(unsafe.Sizeof(*t.nid)) - - return t.nid.add() -} - -func (t *winTray) createMenu() error { - const MIM_APPLYTOSUBMENUS = 0x80000000 // Settings apply to the menu and all of its submenus - - menuHandle, _, err := pCreatePopupMenu.Call() - if menuHandle == 0 { - return err - } - t.menu = windows.Handle(menuHandle) - - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647575(v=vs.85).aspx - mi := struct { - Size, Mask, Style, Max uint32 - Background windows.Handle - ContextHelpID uint32 - MenuData uintptr - }{ - Mask: MIM_APPLYTOSUBMENUS, - } - mi.Size = uint32(unsafe.Sizeof(mi)) - - res, _, err := pSetMenuInfo.Call( - uintptr(t.menu), - uintptr(unsafe.Pointer(&mi)), - ) - if res == 0 { - return err - } - return nil -} - -func (t *winTray) addOrUpdateMenuItem(menuId int32, 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 - ) - const MFT_STRING = 0x00000000 - const ( - MFS_CHECKED = 0x00000008 - MFS_DISABLED = 0x00000003 - ) - titlePtr, err := windows.UTF16PtrFromString(title) - if err != nil { - return err - } - - mi := menuItemInfo{ - Mask: MIIM_FTYPE | MIIM_STRING | MIIM_ID | MIIM_STATE, - Type: MFT_STRING, - ID: uint32(menuId), - TypeData: titlePtr, - Cch: uint32(len(title)), - } - if disabled { - mi.State |= MFS_DISABLED - } - if checked { - mi.State |= MFS_CHECKED - } - mi.Size = uint32(unsafe.Sizeof(mi)) - - // We set the menu item info based on the menuID - res, _, err := pSetMenuItemInfo.Call( - uintptr(t.menu), - uintptr(menuId), - 0, - uintptr(unsafe.Pointer(&mi)), - ) - - if res == 0 { - t.addToVisibleItems(menuId) - position := t.getVisibleItemIndex(menuId) - res, _, err = pInsertMenuItem.Call( - uintptr(t.menu), - uintptr(position), - 1, - uintptr(unsafe.Pointer(&mi)), - ) - if res == 0 { - t.delFromVisibleItems(menuId) - return err - } - } - - return nil -} - -func (t *winTray) addSeparatorMenuItem(menuId int32) error { - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx - const ( - MIIM_FTYPE = 0x00000100 - MIIM_ID = 0x00000002 - MIIM_STATE = 0x00000001 - ) - const MFT_SEPARATOR = 0x00000800 - - mi := menuItemInfo{ - Mask: MIIM_FTYPE | MIIM_ID | MIIM_STATE, - Type: MFT_SEPARATOR, - ID: uint32(menuId), - } - - mi.Size = uint32(unsafe.Sizeof(mi)) - - t.addToVisibleItems(menuId) - position := t.getVisibleItemIndex(menuId) - res, _, err := pInsertMenuItem.Call( - uintptr(t.menu), - uintptr(position), - 1, - uintptr(unsafe.Pointer(&mi)), - ) - if res == 0 { - return err - } - - return nil -} - -func (t *winTray) hideMenuItem(menuId int32) 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 - - res, _, err := pDeleteMenu.Call( - uintptr(t.menu), - uintptr(uint32(menuId)), - MF_BYCOMMAND, - ) - if res == 0 && err.(syscall.Errno) != ERROR_SUCCESS { - return err - } - t.delFromVisibleItems(menuId) - - return nil -} - -func (t *winTray) showMenu() error { - const ( - TPM_BOTTOMALIGN = 0x0020 - TPM_LEFTALIGN = 0x0000 - ) - p := point{} - res, _, err := pGetCursorPos.Call(uintptr(unsafe.Pointer(&p))) - if res == 0 { - return err - } - pSetForegroundWindow.Call(uintptr(t.window)) - - res, _, err = pTrackPopupMenu.Call( - uintptr(t.menu), - TPM_BOTTOMALIGN|TPM_LEFTALIGN, - uintptr(p.X), - uintptr(p.Y), - 0, - uintptr(t.window), - 0, - ) - if res == 0 { - return err - } - - 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:]...) - 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) getVisibleItemIndex(val int32) int { - for i, itemval := range t.visibleItems { - if uint32(val) == itemval { - return i - } - } - return -1 -} - -func nativeLoop() { - if err := wt.initInstance(); err != nil { - log.Errorf("Unable to init instance: %v", err) - return - } - - if err := wt.createMenu(); err != nil { - log.Errorf("Unable to create menu: %v", err) - return - } - - defer func() { - pDestroyWindow.Call(uintptr(wt.window)) - wt.wcex.unregister() - }() - - go systrayReady() - - // Main message pump. - m := &struct { - WindowHandle windows.Handle - Message uint32 - Wparam uintptr - Lparam uintptr - Time uint32 - Pt point - }{} - for { - ret, _, err := pGetMessage.Call(uintptr(unsafe.Pointer(m)), 0, 0, 0) - - // If the function retrieves a message other than WM_QUIT, the return value is nonzero. - // If the function retrieves the WM_QUIT message, the return value is zero. - // If there is an error, the return value is -1 - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644936(v=vs.85).aspx - switch int32(ret) { - case -1: - log.Errorf("Error at message loop: %v", err) - return - case 0: - return - default: - pTranslateMessage.Call(uintptr(unsafe.Pointer(m))) - pDispatchMessage.Call(uintptr(unsafe.Pointer(m))) + if err := mainWindow.SetHeight(height); err != nil { + fail("Unable to set height", err) } } + systrayReady() + mainWindow.Run() } func quit() { - const WM_CLOSE = 0x0010 - - pPostMessage.Call( - uintptr(wt.window), - WM_CLOSE, - 0, - 0, - ) + atomic.StoreInt32(&okayToClose, 1) + mainWindow.Close() + notifyIcon.Dispose() + systrayExit() } // 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) { - 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 - } + filename := "systray.ico" + err := ioutil.WriteFile(filename, iconBytes, 0644) + if err != nil { + fail("Unable to save icon to disk", err) } - - if err := wt.setIcon(iconFilePath); err != nil { - log.Errorf("Unable to set icon: %v", err) - return + defer os.Remove(filename) + icon, err := walk.Resources.Icon(filename) + if err != nil { + fail("Unable to load icon", err) + } + err = notifyIcon.SetIcon(icon) + if err != nil { + fail("Unable to set systray icon", err) + } + err = notifyIcon.SetVisible(true) + if err != nil { + fail("Unable to make systray icon visible", err) } } // 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) SetIcon(iconBytes []byte) { - // do nothing + // not supported on Windows } // SetTooltip sets the systray tooltip to display on mouse hover of the tray icon, // only available on Mac and Windows. func SetTooltip(tooltip string) { - if err := wt.setTooltip(tooltip); err != nil { - log.Errorf("Unable to set tooltip: %v", err) + if err := notifyIcon.SetToolTip(tooltip); err != nil { + fail("Unable to set tooltip", err) + } +} + +// ShowAppWindow shows the given URL in the application window. Only works if +// configureAppWindow has been called first. +func ShowAppWindow(url string) { + if webView == nil { return } + webView.SetURL(url) + mainWindow.SetVisible(true) } func addOrUpdateMenuItem(item *MenuItem) { - err := wt.addOrUpdateMenuItem(item.id, item.title, item.disabled, item.checked) + action := actions[item.id] + if action == nil { + item.id = atomic.AddInt32(&nextActionId, 1) + action = walk.NewAction() + action.Triggered().Attach(func() { + item.ClickedCh <- struct{}{} + }) + if err := notifyIcon.ContextMenu().Actions().Add(action); err != nil { + fail("Unable to add menu item to systray", err) + } + actions[item.id] = action + } + err := action.SetText(item.title) if err != nil { - log.Errorf("Unable to addOrUpdateMenuItem: %v", err) - return + fail("Unable to set menu item text", err) + } + err = action.SetChecked(item.checked) + if err != nil { + fail("Unable to set menu item checked", err) + } + err = action.SetEnabled(!item.Disabled()) + if err != nil { + fail("Unable to set menu item enabled", err) } } -func addSeparator(id int32) { - err := wt.addSeparatorMenuItem(id) +func (item *MenuItem) SetIcon(iconBytes []byte) { + filename := fmt.Sprintf("systray.%d.ico", item.id) + err := ioutil.WriteFile(filename, iconBytes, 0644) if err != nil { - log.Errorf("Unable to addSeparator: %v", err) - return + fail("Unable to save icon to disk", err) + } + defer os.Remove(filename) + icon, err := walk.Resources.Image(filename) + if err != nil { + fail("Unable to load icon", err) + } + actions[item.id].SetImage(icon) +} + +func addSeparator(id int32) { + action := walk.NewSeparatorAction() + if err := notifyIcon.ContextMenu().Actions().Add(action); err != nil { + fail("Unable to add separator", err) } } func hideMenuItem(item *MenuItem) { - err := wt.hideMenuItem(item.id) - if err != nil { - log.Errorf("Unable to hideMenuItem: %v", err) - return - } + actions[item.id].SetVisible(false) } func showMenuItem(item *MenuItem) { - addOrUpdateMenuItem(item) + actions[item.id].SetVisible(true) +} + +func fail(msg string, err error) { + panic(fmt.Errorf("%v: %v", msg, err)) } diff --git a/systray_windows_test.go b/systray_windows_test.go deleted file mode 100644 index 7cb6c75..0000000 --- a/systray_windows_test.go +++ /dev/null @@ -1,132 +0,0 @@ -// +build windows - -package systray - -import ( - "io/ioutil" - "runtime" - "sync/atomic" - "testing" - "time" - "unsafe" - - "golang.org/x/sys/windows" -) - -const iconFilePath = "example/icon/iconwin.ico" - -func TestBaseWindowsTray(t *testing.T) { - systrayReady = func() {} - systrayExit = func() {} - - runtime.LockOSThread() - - if err := wt.initInstance(); err != nil { - t.Fatalf("initInstance failed: %s", err) - } - - if err := wt.createMenu(); err != nil { - t.Fatalf("createMenu failed: %s", err) - } - - defer func() { - pDestroyWindow.Call(uintptr(wt.window)) - wt.wcex.unregister() - }() - - if err := wt.setIcon(iconFilePath); err != nil { - t.Errorf("SetIcon failed: %s", err) - } - - if err := wt.setTooltip("Cyrillic tooltip тест:)"); err != nil { - t.Errorf("SetIcon failed: %s", err) - } - - var id int32 = 0 - err := wt.addOrUpdateMenuItem(atomic.AddInt32(&id, 1), "Simple enabled", false, false) - if err != nil { - t.Errorf("mergeMenuItem failed: %s", err) - } - err = wt.addOrUpdateMenuItem(atomic.AddInt32(&id, 1), "Simple disabled", true, false) - if err != nil { - t.Errorf("mergeMenuItem failed: %s", err) - } - err = wt.addSeparatorMenuItem(atomic.AddInt32(&id, 1)) - if err != nil { - t.Errorf("addSeparatorMenuItem failed: %s", err) - } - err = wt.addOrUpdateMenuItem(atomic.AddInt32(&id, 1), "Simple checked enabled", false, true) - if err != nil { - t.Errorf("mergeMenuItem failed: %s", err) - } - err = wt.addOrUpdateMenuItem(atomic.AddInt32(&id, 1), "Simple checked disabled", true, true) - if err != nil { - t.Errorf("mergeMenuItem failed: %s", err) - } - - err = wt.hideMenuItem(1) - if err != nil { - t.Errorf("hideMenuItem failed: %s", err) - } - - err = wt.hideMenuItem(100) - if err == nil { - t.Error("hideMenuItem failed: must return error on invalid item id") - } - - err = wt.addOrUpdateMenuItem(2, "Simple disabled update", true, false) - if err != nil { - t.Errorf("mergeMenuItem failed: %s", err) - } - - time.AfterFunc(1*time.Second, quit) - - m := struct { - WindowHandle windows.Handle - Message uint32 - Wparam uintptr - Lparam uintptr - Time uint32 - Pt point - }{} - for { - ret, _, err := pGetMessage.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0) - res := int32(ret) - if res == -1 { - t.Errorf("win32 GetMessage failed: %v", err) - return - } else if res == 0 { - break - } - pTranslateMessage.Call(uintptr(unsafe.Pointer(&m))) - pDispatchMessage.Call(uintptr(unsafe.Pointer(&m))) - } -} - -func TestWindowsRun(t *testing.T) { - onReady := func() { - b, err := ioutil.ReadFile(iconFilePath) - if err != nil { - t.Fatalf("Can't load icon file: %v", err) - } - SetIcon(b) - SetTitle("Test title с кириллицей") - - bSomeBtn := AddMenuItem("Йа кнопко", "") - bSomeBtn.Check() - AddSeparator() - bQuit := AddMenuItem("Quit", "Quit the whole app") - go func() { - <-bQuit.ClickedCh - t.Log("Quit reqested") - Quit() - }() - time.AfterFunc(1*time.Second, Quit) - } - - onExit := func() { - t.Log("Exit success") - } - - Run(onReady, onExit) -} From da306118556c93c67aa49a163151f4bbf0b091ed Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Thu, 19 Sep 2019 13:11:49 -0500 Subject: [PATCH 03/11] Updated darwin implementation to meet new API --- systray.h | 2 +- systray_browser_darwin.m | 9 +-------- systray_darwin.m | 7 ++++++- systray_nonwindows.go | 9 ++------- 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/systray.h b/systray.h index cecdc40..128315f 100644 --- a/systray.h +++ b/systray.h @@ -1,7 +1,7 @@ extern void systray_ready(); extern void systray_on_exit(); extern void systray_menu_item_selected(int menu_id); -int nativeLoop(void); +int nativeLoop(char* title, int width, int height); void setIcon(const char* iconBytes, int length); void setMenuItemIcon(const char* iconBytes, int length, int menuId); diff --git a/systray_browser_darwin.m b/systray_browser_darwin.m index c20c7f6..42f04cd 100644 --- a/systray_browser_darwin.m +++ b/systray_browser_darwin.m @@ -5,7 +5,7 @@ NSWindowController *windowController = nil; WKWebView *webView = nil; -void doConfigureAppWindow(char* title, int width, int height) +void configureAppWindow(char* title, int width, int height) { if (windowController != nil) { // already configured, ignore @@ -49,13 +49,6 @@ void doConfigureAppWindow(char* title, int width, int height) free(title); } -void configureAppWindow(char* title, int width, int height) -{ - dispatch_async(dispatch_get_main_queue(), ^{ - doConfigureAppWindow(title, width, height); - }); -} - void doShowAppWindow(char* url) { if (windowController == nil) { diff --git a/systray_darwin.m b/systray_darwin.m index 7189ef5..fff2465 100644 --- a/systray_darwin.m +++ b/systray_darwin.m @@ -187,9 +187,14 @@ @end -int nativeLoop(void) { +void configureAppWindow(char* title, int width, int height); + +int nativeLoop(char* title, int width, int height) { AppDelegate *delegate = [[AppDelegate alloc] init]; [[NSApplication sharedApplication] setDelegate:delegate]; + if (strcmp(title, "") != 0) { + configureAppWindow(title, width, height); + } [NSApp run]; return EXIT_SUCCESS; } diff --git a/systray_nonwindows.go b/systray_nonwindows.go index fdacfa2..598df9e 100644 --- a/systray_nonwindows.go +++ b/systray_nonwindows.go @@ -16,19 +16,14 @@ import ( "unsafe" ) -func nativeLoop() { - C.nativeLoop() +func nativeLoop(title string, width int, height int) { + C.nativeLoop(C.CString(title), C.int(width), C.int(height)) } func quit() { C.quit() } -// EnableAppWindow enables a single application window for this app. -func EnableAppWindow(title string, width int, height int) { - C.configureAppWindow(C.CString(title), C.int(width), C.int(height)) -} - // ShowAppWindow shows the given URL in the application window. Only works if // configureAppWindow has been called first. func ShowAppWindow(url string) { From fdf131d2a9d9bb2313c9e0424ab8a38926a6e732 Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Thu, 19 Sep 2019 13:39:06 -0500 Subject: [PATCH 04/11] Added note about windows manifest to readme --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index be16646..67a530d 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,35 @@ To prevent launching a console window when running on Windows, add these command go build -ldflags -H=windowsgui ``` +You'll also need to include a Manifest for your windows app. Assuming your executable is named `app.exe`, create a file `app.manifest` like this: + +``` + + + + + + + + + + + PerMonitorV2, PerMonitor + True + + + +``` + +Then either compile the manifest using the [rsrc](https://github.com/akavel/rsrc) tool, like this: + +``` +go get github.com/akavel/rsrc +rsrc -manifest app.manifest -o rsrc.syso +``` + +or rename the app.manifest file to app.exe.manifest and distribute it with the application instead. + ### macOS On macOS, you will need to create an application bundle to wrap the binary; simply folders with the following minimal structure and assets: From ba2fcb6d02995705732619d6373f7df757554fb6 Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Fri, 20 Sep 2019 10:22:15 -0500 Subject: [PATCH 05/11] Application window working on Linux --- systray_browser_linux.c | 40 ++++++++++++++++++++++++---------------- systray_linux.c | 6 ++++-- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/systray_browser_linux.c b/systray_browser_linux.c index af32294..566d1fa 100644 --- a/systray_browser_linux.c +++ b/systray_browser_linux.c @@ -4,13 +4,24 @@ static GtkWidget *web_window = NULL; static WebKitWebView *webView = NULL; -static gboolean closeWebViewCb(WebKitWebView* webView, GtkWidget* window); +static gint x, y; +static bool hasPosition = false; -void prepareBrowser() +gboolean on_window_deleted(GtkWidget *window, GdkEvent *event, gpointer data) +{ + gtk_window_get_position(GTK_WINDOW(window), &x, &y); + hasPosition = 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_new(GTK_WINDOW_TOPLEVEL); - gtk_window_set_default_size(GTK_WINDOW(web_window), 800, 600); + gtk_window_set_title(GTK_WINDOW(web_window), title); + gtk_window_set_default_size(GTK_WINDOW(web_window), width, height); + g_signal_connect(G_OBJECT(web_window), "delete-event", G_CALLBACK(on_window_deleted), NULL); // Create a browser instance webView = WEBKIT_WEB_VIEW(webkit_web_view_new()); @@ -18,29 +29,26 @@ void prepareBrowser() // Put the browser area into the web window gtk_container_add(GTK_CONTAINER(web_window), GTK_WIDGET(webView)); - g_signal_connect(webView, "close", G_CALLBACK(closeWebViewCb), web_window); - // Make sure that when the browser area becomes visible, it will get mouse // and keyboard events gtk_widget_grab_focus(GTK_WIDGET(webView)); + free(title); } -gboolean do_open_in_browser(gpointer data) +gboolean do_show_app_window(gpointer data) { gtk_widget_show_all(web_window); - return TRUE; + if (hasPosition) { + gtk_window_move(GTK_WINDOW(web_window), x, y); + } + return FALSE; } -void openInBrowser(char* url) +void showAppWindow(char* url) { - // Load a web page into the browser instance + // Load a web page into the browser instance webkit_web_view_load_uri(webView, url); - gdk_threads_add_idle(do_open_in_browser, url); -} - -static gboolean closeWebViewCb(WebKitWebView* webView, GtkWidget* window) -{ - gtk_widget_destroy(window); - return TRUE; + gdk_threads_add_idle(do_show_app_window, NULL); + free(url); } \ No newline at end of file diff --git a/systray_linux.c b/systray_linux.c index 20eedaf..7b045f2 100644 --- a/systray_linux.c +++ b/systray_linux.c @@ -25,7 +25,7 @@ typedef struct { void prepareBrowser(); -int nativeLoop(void) { +int nativeLoop(char* title, int width, int height) { gtk_init(0, NULL); global_app_indicator = app_indicator_new("systray", "", APP_INDICATOR_CATEGORY_APPLICATION_STATUS); @@ -33,7 +33,9 @@ int nativeLoop(void) { global_tray_menu = gtk_menu_new(); app_indicator_set_menu(global_app_indicator, GTK_MENU(global_tray_menu)); systray_ready(); - prepareBrowser(); + if (strcmp(title, "") != 0) { + configureAppWindow(title, width, height); + } gtk_main(); systray_on_exit(); return 0; From 569831f9848613a6a2be8c3292fb67b622084d31 Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Fri, 20 Sep 2019 10:57:20 -0500 Subject: [PATCH 06/11] Not reshowing app window if already shown --- systray_browser_linux.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/systray_browser_linux.c b/systray_browser_linux.c index 566d1fa..d44eb5a 100644 --- a/systray_browser_linux.c +++ b/systray_browser_linux.c @@ -4,6 +4,7 @@ static GtkWidget *web_window = NULL; static WebKitWebView *webView = NULL; +static bool appShown = false; static gint x, y; static bool hasPosition = false; @@ -12,6 +13,7 @@ gboolean on_window_deleted(GtkWidget *window, GdkEvent *event, gpointer data) gtk_window_get_position(GTK_WINDOW(window), &x, &y); hasPosition = true; gtk_widget_hide(window); + appShown = false; return TRUE; } @@ -37,9 +39,12 @@ void configureAppWindow(char* title, int width, int height) gboolean do_show_app_window(gpointer data) { - gtk_widget_show_all(web_window); - if (hasPosition) { - gtk_window_move(GTK_WINDOW(web_window), x, y); + if (!appShown) { + gtk_widget_show_all(web_window); + if (hasPosition) { + gtk_window_move(GTK_WINDOW(web_window), x, y); + } + appShown = true; } return FALSE; } From b28003b454a3293d2d2238e471cd188005c80187 Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Fri, 20 Sep 2019 11:03:02 -0500 Subject: [PATCH 07/11] More linux fixes for app window --- systray_browser_linux.c | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/systray_browser_linux.c b/systray_browser_linux.c index d44eb5a..a34d28a 100644 --- a/systray_browser_linux.c +++ b/systray_browser_linux.c @@ -1,8 +1,8 @@ #include #include -static GtkWidget *web_window = NULL; -static WebKitWebView *webView = NULL; +static GtkWindow *web_window = NULL; +static WebKitWebView *web_view = NULL; static bool appShown = false; static gint x, y; @@ -20,31 +20,33 @@ gboolean on_window_deleted(GtkWidget *window, GdkEvent *event, gpointer data) void configureAppWindow(char* title, int width, int height) { // Create an 800x600 window that will contain the browser instance - web_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); - gtk_window_set_title(GTK_WINDOW(web_window), title); - gtk_window_set_default_size(GTK_WINDOW(web_window), width, height); + 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); g_signal_connect(G_OBJECT(web_window), "delete-event", G_CALLBACK(on_window_deleted), NULL); // Create a browser instance - webView = WEBKIT_WEB_VIEW(webkit_web_view_new()); + 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(webView)); + 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(webView)); + gtk_widget_grab_focus(GTK_WIDGET(web_view)); free(title); } gboolean do_show_app_window(gpointer data) { if (!appShown) { - gtk_widget_show_all(web_window); + gtk_widget_show_all(GTK_WIDGET(web_window)); if (hasPosition) { - gtk_window_move(GTK_WINDOW(web_window), x, y); + gtk_window_move(web_window, x, y); } appShown = true; + } else { + gtk_window_present(web_window); } return FALSE; } @@ -52,7 +54,7 @@ gboolean do_show_app_window(gpointer data) void showAppWindow(char* url) { // Load a web page into the browser instance - webkit_web_view_load_uri(webView, url); + webkit_web_view_load_uri(web_view, url); gdk_threads_add_idle(do_show_app_window, NULL); free(url); From 83e791c612ecd412e8282e0058a18459658d84d7 Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Fri, 20 Sep 2019 11:13:40 -0500 Subject: [PATCH 08/11] More linux fixes for app window --- systray_browser_linux.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/systray_browser_linux.c b/systray_browser_linux.c index a34d28a..cff51c3 100644 --- a/systray_browser_linux.c +++ b/systray_browser_linux.c @@ -23,6 +23,7 @@ void configureAppWindow(char* title, int width, int height) 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 @@ -45,7 +46,7 @@ gboolean do_show_app_window(gpointer data) gtk_window_move(web_window, x, y); } appShown = true; - } else { + } else if (!gtk_window_is_active(web_window)) { gtk_window_present(web_window); } return FALSE; From efe21586c2ecb08d72f2ccd85dd1ef5980dc72c8 Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Fri, 20 Sep 2019 11:17:09 -0500 Subject: [PATCH 09/11] More linux fixes for app window --- systray_browser_linux.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/systray_browser_linux.c b/systray_browser_linux.c index cff51c3..b32646c 100644 --- a/systray_browser_linux.c +++ b/systray_browser_linux.c @@ -57,6 +57,6 @@ void showAppWindow(char* url) // Load a web page into the browser instance webkit_web_view_load_uri(web_view, url); - gdk_threads_add_idle(do_show_app_window, NULL); + g_idle_add(do_show_app_window, NULL); free(url); } \ No newline at end of file From f26b160dac3b22b37b8e4c3589ccf46bf4a761db Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Fri, 20 Sep 2019 11:21:48 -0500 Subject: [PATCH 10/11] More linux fixes for app window --- systray_browser_linux.c | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/systray_browser_linux.c b/systray_browser_linux.c index b32646c..493d980 100644 --- a/systray_browser_linux.c +++ b/systray_browser_linux.c @@ -4,16 +4,14 @@ static GtkWindow *web_window = NULL; static WebKitWebView *web_view = NULL; -static bool appShown = false; static gint x, y; -static bool hasPosition = false; +static bool needsMove = false; gboolean on_window_deleted(GtkWidget *window, GdkEvent *event, gpointer data) { gtk_window_get_position(GTK_WINDOW(window), &x, &y); - hasPosition = true; + needsMove = true; gtk_widget_hide(window); - appShown = false; return TRUE; } @@ -40,15 +38,12 @@ void configureAppWindow(char* title, int width, int height) gboolean do_show_app_window(gpointer data) { - if (!appShown) { - gtk_widget_show_all(GTK_WIDGET(web_window)); - if (hasPosition) { - gtk_window_move(web_window, x, y); - } - appShown = true; - } else if (!gtk_window_is_active(web_window)) { - gtk_window_present(web_window); + 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; } From 9b09fbc09c10f7d4a9840a6f88d1a4375b01d3b2 Mon Sep 17 00:00:00 2001 From: Ox Cart Date: Fri, 1 Nov 2019 09:20:22 -0500 Subject: [PATCH 11/11] Explicitly typing NSWindow for browser on OS X --- systray_browser_darwin.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/systray_browser_darwin.m b/systray_browser_darwin.m index 42f04cd..ca7fa18 100644 --- a/systray_browser_darwin.m +++ b/systray_browser_darwin.m @@ -14,7 +14,7 @@ void configureAppWindow(char* title, int width, int height) NSRect frame = NSMakeRect(0, 0, width, height); int mask = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable | NSWindowStyleMaskClosable; - id window = + NSWindow *window = [[NSWindow alloc] initWithContentRect:frame styleMask:mask backing:NSBackingStoreBuffered