Merge pull request #97 from getlantern/ox/issue452

Add support for app window
This commit is contained in:
Percy Wegmann
2019-11-11 13:02:43 -06:00
committed by GitHub
15 changed files with 380 additions and 807 deletions
+29
View File
@@ -72,6 +72,35 @@ sudo apt-get install libgtk-3-dev libappindicator3-dev
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:
```
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity version="1.0.0.0" processorArchitecture="*" name="App Name Here" type="win32"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">True</dpiAware>
</windowsSettings>
</application>
</assembly>
```
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:
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity version="1.0.0.0" processorArchitecture="*" name="Lantern" type="win32"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">True</dpiAware>
</windowsSettings>
</application>
</assembly>
+3 -4
View File
@@ -7,7 +7,6 @@ import (
"github.com/getlantern/systray"
"github.com/getlantern/systray/example/icon"
"github.com/skratchdot/open-golang/open"
)
func main() {
@@ -18,7 +17,7 @@ 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() {
@@ -42,7 +41,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 +66,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()
BIN
View File
Binary file not shown.
+13
View File
@@ -0,0 +1,13 @@
module github.com/getlantern/systray
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
)
+33
View File
@@ -0,0 +1,33 @@
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/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=
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=
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=
+7 -3
View File
@@ -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
+3 -1
View File
@@ -1,12 +1,14 @@
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);
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);
+72
View File
@@ -0,0 +1,72 @@
#import <Cocoa/Cocoa.h>
#import <WebKit/WebKit.h>
#include "systray.h"
NSWindowController *windowController = nil;
WKWebView *webView = nil;
void configureAppWindow(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;
NSWindow *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 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);
});
}
+57
View File
@@ -0,0 +1,57 @@
#include <gtk/gtk.h>
#include <webkit2/webkit2.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);
}
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);
}
+11 -1
View File
@@ -70,6 +70,11 @@
systray_ready();
}
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
{
return FALSE;
}
- (void)applicationWillTerminate:(NSNotification *)aNotification
{
systray_on_exit();
@@ -182,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;
}
+6 -1
View File
@@ -23,7 +23,9 @@ typedef struct {
short checked;
} MenuItemInfo;
int nativeLoop(void) {
void prepareBrowser();
int nativeLoop(char* title, int width, int height) {
gtk_init(0, NULL);
global_app_indicator = app_indicator_new("systray", "",
APP_INDICATOR_CATEGORY_APPLICATION_STATUS);
@@ -31,6 +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();
if (strcmp(title, "") != 0) {
configureAppWindow(title, width, height);
}
gtk_main();
systray_on_exit();
return 0;
+11 -4
View File
@@ -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"
*/
@@ -15,14 +16,20 @@ 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()
}
// 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.
+120 -661
View File
File diff suppressed because it is too large Load Diff
-132
View File
@@ -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)
}