diff options
-rw-r--r-- | vendor.json | 9 | ||||
-rw-r--r-- | vendor/README | 3 | ||||
-rw-r--r-- | vendor/github.com/getlantern/systray/.gitignore | 11 | ||||
-rw-r--r-- | vendor/github.com/getlantern/systray/LICENSE | 202 | ||||
-rw-r--r-- | vendor/github.com/getlantern/systray/README.md | 50 | ||||
-rw-r--r-- | vendor/github.com/getlantern/systray/systray.go | 181 | ||||
-rw-r--r-- | vendor/github.com/getlantern/systray/systray.h | 14 | ||||
-rw-r--r-- | vendor/github.com/getlantern/systray/systray_darwin.m | 249 | ||||
-rw-r--r-- | vendor/github.com/getlantern/systray/systray_linux.c | 217 | ||||
-rw-r--r-- | vendor/github.com/getlantern/systray/systray_nonwindows.go | 99 | ||||
-rw-r--r-- | vendor/github.com/getlantern/systray/systray_windows.go | 718 | ||||
-rw-r--r-- | vendor/github.com/getlantern/systray/systray_windows_test.go | 132 |
12 files changed, 1885 insertions, 0 deletions
diff --git a/vendor.json b/vendor.json new file mode 100644 index 0000000..14c7610 --- /dev/null +++ b/vendor.json @@ -0,0 +1,9 @@ +{ + "comment": "", + "package": [ + { + "path": "github.com/getlantern/systray" + } + ] +} + diff --git a/vendor/README b/vendor/README new file mode 100644 index 0000000..de0db4e --- /dev/null +++ b/vendor/README @@ -0,0 +1,3 @@ +2018-11-14 +----------------- +* vendor getlantern/systray with PR #74, to work around menu positioning bug in windows. diff --git a/vendor/github.com/getlantern/systray/.gitignore b/vendor/github.com/getlantern/systray/.gitignore new file mode 100644 index 0000000..ae7e06b --- /dev/null +++ b/vendor/github.com/getlantern/systray/.gitignore @@ -0,0 +1,11 @@ +example/example +*~ +*.swp +*.exe +Release +Debug +*.sdf +dll/systray_unsigned.dll +out.txt +.vs +on_exit*.txt diff --git a/vendor/github.com/getlantern/systray/LICENSE b/vendor/github.com/getlantern/systray/LICENSE new file mode 100644 index 0000000..3ee0162 --- /dev/null +++ b/vendor/github.com/getlantern/systray/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2014 Brave New Software Project, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/getlantern/systray/README.md b/vendor/github.com/getlantern/systray/README.md new file mode 100644 index 0000000..626c132 --- /dev/null +++ b/vendor/github.com/getlantern/systray/README.md @@ -0,0 +1,50 @@ +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. + +## Usage +```go +func main() { + // Should be called at the very beginning of main(). + systray.Run(onReady, onExit) +} + +func onReady() { + systray.SetIcon(icon.Data) + systray.SetTitle("Awesome App") + systray.SetTooltip("Pretty awesome超级棒") + mQuit := systray.AddMenuItem("Quit", "Quit the whole app") + + // Sets the icon of a menu item. Only available on Mac. + mQuit.SetIcon(icon.Data) +} + +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 + +### 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. + +```sh +go get +go run main.go +``` + +## Credits + +- https://github.com/xilp/systray +- https://github.com/cratonica/trayhost diff --git a/vendor/github.com/getlantern/systray/systray.go b/vendor/github.com/getlantern/systray/systray.go new file mode 100644 index 0000000..d433173 --- /dev/null +++ b/vendor/github.com/getlantern/systray/systray.go @@ -0,0 +1,181 @@ +/* +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 + +import ( + "runtime" + "sync" + "sync/atomic" + + "github.com/getlantern/golog" +) + +var ( + hasStarted = int64(0) + hasQuit = int64(0) +) + +// 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 + ClickedCh chan struct{} + + // id uniquely identify a menu item, not supposed to be modified + id int32 + // title is the text shown on menu item + title string + // tooltip is the text shown when pointing to menu item + tooltip string + // disabled menu item is grayed out and has no effect when clicked + disabled bool + // checked menu item has a tick before the title + checked bool +} + +var ( + log = golog.LoggerFor("systray") + + systrayReady func() + systrayExit func() + menuItems = make(map[int32]*MenuItem) + menuItemsLock sync.RWMutex + + currentID = int32(-1) +) + +// 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. +func Run(onReady func(), onExit func()) { + runtime.LockOSThread() + atomic.StoreInt64(&hasStarted, 1) + + if onReady == nil { + systrayReady = func() {} + } else { + // Run onReady on separate goroutine to avoid blocking event loop + readyCh := make(chan interface{}) + go func() { + <-readyCh + onReady() + }() + systrayReady = 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() +} + +// Quit the systray +func Quit() { + if atomic.LoadInt64(&hasStarted) == 1 && atomic.CompareAndSwapInt64(&hasQuit, 0, 1) { + quit() + } +} + +// AddMenuItem adds menu item with designated title and tooltip, returning a channel +// that notifies whenever that menu item is clicked. +// +// 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.update() + return item +} + +// AddSeparator adds a separator bar to the menu +func AddSeparator() { + addSeparator(atomic.AddInt32(¤tID, 1)) +} + +// SetTitle set the text to display on a menu item +func (item *MenuItem) SetTitle(title string) { + item.title = title + item.update() +} + +// SetTooltip set the tooltip to show when mouse hover +func (item *MenuItem) SetTooltip(tooltip string) { + item.tooltip = tooltip + item.update() +} + +// Disabled checkes if the menu item is disabled +func (item *MenuItem) Disabled() bool { + return item.disabled +} + +// Enable a menu item regardless if it's previously enabled or not +func (item *MenuItem) Enable() { + item.disabled = false + item.update() +} + +// Disable a menu item regardless if it's previously disabled or not +func (item *MenuItem) Disable() { + item.disabled = true + item.update() +} + +// Hide hides a menu item +func (item *MenuItem) Hide() { + hideMenuItem(item) +} + +// Show shows a previously hidden menu item +func (item *MenuItem) Show() { + showMenuItem(item) +} + +// Checked returns if the menu item has a check mark +func (item *MenuItem) Checked() bool { + return item.checked +} + +// Check a menu item regardless if it's previously checked or not +func (item *MenuItem) Check() { + item.checked = true + item.update() +} + +// Uncheck a menu item regardless if it's previously unchecked or not +func (item *MenuItem) Uncheck() { + item.checked = false + item.update() +} + +// update propogates changes on a menu item to systray +func (item *MenuItem) update() { + menuItemsLock.Lock() + defer menuItemsLock.Unlock() + menuItems[item.id] = item + addOrUpdateMenuItem(item) +} + +func systrayMenuItemSelected(id int32) { + menuItemsLock.RLock() + item := menuItems[id] + menuItemsLock.RUnlock() + select { + case item.ClickedCh <- struct{}{}: + // in case no one waiting for the channel + default: + } +} diff --git a/vendor/github.com/getlantern/systray/systray.h b/vendor/github.com/getlantern/systray/systray.h new file mode 100644 index 0000000..36bcf98 --- /dev/null +++ b/vendor/github.com/getlantern/systray/systray.h @@ -0,0 +1,14 @@ +extern void systray_ready(); +extern void systray_on_exit(); +extern void systray_menu_item_selected(int menu_id); +int nativeLoop(void); + +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 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); +void show_menu_item(int menuId); +void quit(); diff --git a/vendor/github.com/getlantern/systray/systray_darwin.m b/vendor/github.com/getlantern/systray/systray_darwin.m new file mode 100644 index 0000000..3d92868 --- /dev/null +++ b/vendor/github.com/getlantern/systray/systray_darwin.m @@ -0,0 +1,249 @@ +#import <Cocoa/Cocoa.h> +#include "systray.h" + +#ifndef NSControlStateValueOff + #define NSControlStateValueOff NSOffState +#endif + +#ifndef NSControlStateValueOn + #define NSControlStateValueOn NSOnState +#endif + +@interface MenuItem : NSObject +{ + @public + NSNumber* menuId; + NSString* title; + NSString* tooltip; + short disabled; + short checked; +} +-(id) initWithId: (int)theMenuId + withTitle: (const char*)theTitle + withTooltip: (const char*)theTooltip + withDisabled: (short)theDisabled + withChecked: (short)theChecked; + @end + @implementation MenuItem + -(id) initWithId: (int)theMenuId + withTitle: (const char*)theTitle + withTooltip: (const char*)theTooltip + withDisabled: (short)theDisabled + withChecked: (short)theChecked +{ + menuId = [NSNumber numberWithInt:theMenuId]; + title = [[NSString alloc] initWithCString:theTitle + encoding:NSUTF8StringEncoding]; + tooltip = [[NSString alloc] initWithCString:theTooltip + encoding:NSUTF8StringEncoding]; + disabled = theDisabled; + checked = theChecked; + return self; +} +@end + +@interface AppDelegate: NSObject <NSApplicationDelegate> + - (void) add_or_update_menu_item:(MenuItem*) item; + - (IBAction)menuHandler:(id)sender; + @property (assign) IBOutlet NSWindow *window; + @end + + @implementation AppDelegate +{ + NSStatusItem *statusItem; + NSMenu *menu; + NSCondition* cond; +} + +@synthesize window = _window; + +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification +{ + self->statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength]; + self->menu = [[NSMenu alloc] init]; + [self->menu setAutoenablesItems: FALSE]; + [self->statusItem setMenu:self->menu]; + systray_ready(); +} + +- (void)applicationWillTerminate:(NSNotification *)aNotification +{ + systray_on_exit(); +} + +- (void)setIcon:(NSImage *)image { + statusItem.button.image = image; + [self updateTitleButtonStyle]; +} + +- (void)setTitle:(NSString *)title { + statusItem.button.title = title; + [self updateTitleButtonStyle]; +} + +-(void)updateTitleButtonStyle { + if (statusItem.button.image != nil) { + if ([statusItem.button.title length] == 0) { + statusItem.button.imagePosition = NSImageOnly; + } else { + statusItem.button.imagePosition = NSImageLeft; + } + } else { + statusItem.button.imagePosition = NSNoImage; + } +} + + +- (void)setTooltip:(NSString *)tooltip { + statusItem.button.toolTip = tooltip; +} + +- (IBAction)menuHandler:(id)sender +{ + NSNumber* menuId = [sender representedObject]; + 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]; + + } + else { + menuItem = [menu itemAtIndex: existedMenuIndex]; + [menuItem setTitle:item->title]; + } + [menuItem setToolTip:item->tooltip]; + if (item->disabled == 1) { + menuItem.enabled = FALSE; + } else { + menuItem.enabled = TRUE; + } + if (item->checked == 1) { + menuItem.state = NSControlStateValueOn; + } else { + menuItem.state = NSControlStateValueOff; + } +} + +- (void) add_separator:(NSNumber*) menuId +{ + [menu addItem: [NSMenuItem separatorItem]]; +} + +- (void) hide_menu_item:(NSNumber*) menuId +{ + NSMenuItem* menuItem; + int existedMenuIndex = [menu indexOfItemWithRepresentedObject: menuId]; + if (existedMenuIndex == -1) { + return; + } + menuItem = [menu itemAtIndex: existedMenuIndex]; + [menuItem setHidden:TRUE]; +} + +- (void)setMenuItemIcon:(NSArray*)imageAndMenuId { + NSImage* image = [imageAndMenuId objectAtIndex:0]; + NSNumber* menuId = [imageAndMenuId objectAtIndex:1]; + + NSMenuItem* menuItem; + int existedMenuIndex = [menu indexOfItemWithRepresentedObject: menuId]; + if (existedMenuIndex == -1) { + 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; + } + menuItem = [menu itemAtIndex: existedMenuIndex]; + [menuItem setHidden:FALSE]; +} + +- (void) quit +{ + [NSApp terminate:self]; +} + +@end + +int nativeLoop(void) { + AppDelegate *delegate = [[AppDelegate alloc] init]; + [[NSApplication sharedApplication] setDelegate:delegate]; + [NSApp run]; + return EXIT_SUCCESS; +} + +void runInMainThread(SEL method, id object) { + [(AppDelegate*)[NSApp delegate] + performSelectorOnMainThread:method + withObject:object + waitUntilDone: YES]; +} + +void setIcon(const char* iconBytes, int length) { + NSData* buffer = [NSData dataWithBytes: iconBytes length:length]; + NSImage *image = [[NSImage alloc] initWithData:buffer]; + [image setSize:NSMakeSize(16, 16)]; + runInMainThread(@selector(setIcon:), (id)image); +} + +void setMenuItemIcon(const char* iconBytes, int length, int menuId) { + NSData* buffer = [NSData dataWithBytes: iconBytes length:length]; + NSImage *image = [[NSImage alloc] initWithData:buffer]; + [image setSize:NSMakeSize(16, 16)]; + + NSNumber *mId = [NSNumber numberWithInt:menuId]; + runInMainThread(@selector(setMenuItemIcon:), @[image, (id)mId]); +} + +void setTitle(char* ctitle) { + NSString* title = [[NSString alloc] initWithCString:ctitle + encoding:NSUTF8StringEncoding]; + free(ctitle); + runInMainThread(@selector(setTitle:), (id)title); +} + +void setTooltip(char* ctooltip) { + NSString* tooltip = [[NSString alloc] initWithCString:ctooltip + encoding:NSUTF8StringEncoding]; + free(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]; + free(title); + free(tooltip); + runInMainThread(@selector(add_or_update_menu_item:), (id)item); +} + +void add_separator(int menuId) { + NSNumber *mId = [NSNumber numberWithInt:menuId]; + runInMainThread(@selector(add_separator:), (id)mId); +} + +void hide_menu_item(int menuId) { + NSNumber *mId = [NSNumber numberWithInt:menuId]; + runInMainThread(@selector(hide_menu_item:), (id)mId); +} + +void show_menu_item(int menuId) { + NSNumber *mId = [NSNumber numberWithInt:menuId]; + runInMainThread(@selector(show_menu_item:), (id)mId); +} + +void quit() { + runInMainThread(@selector(quit), nil); +} diff --git a/vendor/github.com/getlantern/systray/systray_linux.c b/vendor/github.com/getlantern/systray/systray_linux.c new file mode 100644 index 0000000..72cd614 --- /dev/null +++ b/vendor/github.com/getlantern/systray/systray_linux.c @@ -0,0 +1,217 @@ +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include <limits.h> +#include <libappindicator/app-indicator.h> +#include "systray.h" + +static AppIndicator *global_app_indicator; +static GtkWidget *global_tray_menu = NULL; +static GList *global_menu_items = NULL; +static char temp_file_name[PATH_MAX] = ""; + +typedef struct { + GtkWidget *menu_item; + int menu_id; +} MenuItemNode; + +typedef struct { + int menu_id; + char* title; + char* tooltip; + short disabled; + short checked; +} MenuItemInfo; + +int nativeLoop(void) { + gtk_init(0, NULL); + global_app_indicator = app_indicator_new("systray", "", + APP_INDICATOR_CATEGORY_APPLICATION_STATUS); + app_indicator_set_status(global_app_indicator, APP_INDICATOR_STATUS_ACTIVE); + global_tray_menu = gtk_menu_new(); + app_indicator_set_menu(global_app_indicator, GTK_MENU(global_tray_menu)); + systray_ready(); + gtk_main(); + systray_on_exit(); + return 0; +} + +void _unlink_temp_file() { + if (strlen(temp_file_name) != 0) { + int ret = unlink(temp_file_name); + if (ret == -1) { + printf("failed to remove temp icon file %s: %s\n", temp_file_name, strerror(errno)); + } + temp_file_name[0] = '\0'; + } +} + +// runs in main thread, should always return FALSE to prevent gtk to execute it again +gboolean do_set_icon(gpointer data) { + _unlink_temp_file(); + char *tmpdir = getenv("TMPDIR"); + if (NULL == tmpdir) { + tmpdir = "/tmp"; + } + strncpy(temp_file_name, tmpdir, PATH_MAX-1); + strncat(temp_file_name, "/systray_XXXXXX", PATH_MAX-1); + temp_file_name[PATH_MAX-1] = '\0'; + + GBytes* bytes = (GBytes*)data; + int fd = mkstemp(temp_file_name); + if (fd == -1) { + printf("failed to create temp icon file %s: %s\n", temp_file_name, strerror(errno)); + return FALSE; + } + gsize size = 0; + gconstpointer icon_data = g_bytes_get_data(bytes, &size); + ssize_t written = write(fd, icon_data, size); + close(fd); + if(written != size) { + printf("failed to write temp icon file %s: %s\n", temp_file_name, strerror(errno)); + return FALSE; + } + app_indicator_set_icon_full(global_app_indicator, temp_file_name, ""); + app_indicator_set_attention_icon_full(global_app_indicator, temp_file_name, ""); + g_bytes_unref(bytes); + return FALSE; +} + +void _systray_menu_item_selected(int *id) { + systray_menu_item_selected(*id); +} + +// runs in main thread, should always return FALSE to prevent gtk to execute it again +gboolean do_add_or_update_menu_item(gpointer data) { + MenuItemInfo *mii = (MenuItemInfo*)data; + GList* it; + for(it = global_menu_items; it != NULL; it = it->next) { + MenuItemNode* item = (MenuItemNode*)(it->data); + if(item->menu_id == mii->menu_id){ + gtk_menu_item_set_label(GTK_MENU_ITEM(item->menu_item), mii->title); + break; + } + } + + // menu id doesn't exist, add new item + if(it == NULL) { + GtkWidget *menu_item = gtk_menu_item_new_with_label(mii->title); + int *id = malloc(sizeof(int)); + *id = mii->menu_id; + g_signal_connect_swapped(G_OBJECT(menu_item), "activate", G_CALLBACK(_systray_menu_item_selected), id); + gtk_menu_shell_append(GTK_MENU_SHELL(global_tray_menu), menu_item); + + MenuItemNode* new_item = malloc(sizeof(MenuItemNode)); + new_item->menu_id = mii->menu_id; + new_item->menu_item = menu_item; + GList* new_node = malloc(sizeof(GList)); + new_node->data = new_item; + new_node->next = global_menu_items; + if(global_menu_items != NULL) { + global_menu_items->prev = new_node; + } + global_menu_items = new_node; + it = new_node; + } + GtkWidget * menu_item = GTK_WIDGET(((MenuItemNode*)(it->data))->menu_item); + gtk_widget_set_sensitive(menu_item, mii->disabled == 1 ? FALSE : TRUE); + gtk_widget_show(menu_item); + + free(mii->title); + free(mii->tooltip); + free(mii); + return FALSE; +} + +gboolean do_add_separator(gpointer data) { + GtkWidget *separator = gtk_separator_menu_item_new(); + gtk_menu_shell_append(GTK_MENU_SHELL(global_tray_menu), separator); + gtk_widget_show(separator); + return FALSE; +} + +// runs in main thread, should always return FALSE to prevent gtk to execute it again +gboolean do_hide_menu_item(gpointer data) { + MenuItemInfo *mii = (MenuItemInfo*)data; + GList* it; + for(it = global_menu_items; it != NULL; it = it->next) { + MenuItemNode* item = (MenuItemNode*)(it->data); + if(item->menu_id == mii->menu_id){ + gtk_widget_hide(GTK_WIDGET(item->menu_item)); + break; + } + } + return FALSE; +} + +// runs in main thread, should always return FALSE to prevent gtk to execute it again +gboolean do_show_menu_item(gpointer data) { + MenuItemInfo *mii = (MenuItemInfo*)data; + GList* it; + for(it = global_menu_items; it != NULL; it = it->next) { + MenuItemNode* item = (MenuItemNode*)(it->data); + if(item->menu_id == mii->menu_id){ + gtk_widget_show(GTK_WIDGET(item->menu_item)); + break; + } + } + return FALSE; +} + +// runs in main thread, should always return FALSE to prevent gtk to execute it again +gboolean do_quit(gpointer data) { + _unlink_temp_file(); + // app indicator doesn't provide a way to remove it, hide it as a workaround + app_indicator_set_status(global_app_indicator, APP_INDICATOR_STATUS_PASSIVE); + gtk_main_quit(); + return FALSE; +} + +void setIcon(const char* iconBytes, int length) { + GBytes* bytes = g_bytes_new_static(iconBytes, length); + g_idle_add(do_set_icon, bytes); +} + +void setTitle(char* ctitle) { + app_indicator_set_label(global_app_indicator, ctitle, ""); + free(ctitle); +} + +void setTooltip(char* ctooltip) { + free(ctooltip); +} + +void setMenuItemIcon(const char* iconBytes, int length, int menuId) { +} + +void add_or_update_menu_item(int menu_id, char* title, char* tooltip, short disabled, short checked) { + MenuItemInfo *mii = malloc(sizeof(MenuItemInfo)); + mii->menu_id = menu_id; + mii->title = title; + mii->tooltip = tooltip; + mii->disabled = disabled; + mii->checked = checked; + g_idle_add(do_add_or_update_menu_item, mii); +} + +void add_separator(int menu_id) { + MenuItemInfo *mii = malloc(sizeof(MenuItemInfo)); + mii->menu_id = menu_id; + g_idle_add(do_add_separator, mii); +} + +void hide_menu_item(int menu_id) { + MenuItemInfo *mii = malloc(sizeof(MenuItemInfo)); + mii->menu_id = menu_id; + g_idle_add(do_hide_menu_item, mii); +} + +void show_menu_item(int menu_id) { + MenuItemInfo *mii = malloc(sizeof(MenuItemInfo)); + mii->menu_id = menu_id; + g_idle_add(do_show_menu_item, mii); +} + +void quit() { + g_idle_add(do_quit, NULL); +} diff --git a/vendor/github.com/getlantern/systray/systray_nonwindows.go b/vendor/github.com/getlantern/systray/systray_nonwindows.go new file mode 100644 index 0000000..4868b55 --- /dev/null +++ b/vendor/github.com/getlantern/systray/systray_nonwindows.go @@ -0,0 +1,99 @@ +// +build !windows + +package systray + +/* +#cgo linux pkg-config: gtk+-3.0 appindicator3-0.1 +#cgo darwin CFLAGS: -DDARWIN -x objective-c -fobjc-arc +#cgo darwin LDFLAGS: -framework Cocoa + +#include "systray.h" +*/ +import "C" + +import ( + "unsafe" +) + +func nativeLoop() { + C.nativeLoop() +} + +func quit() { + C.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) { + cstr := (*C.char)(unsafe.Pointer(&iconBytes[0])) + C.setIcon(cstr, (C.int)(len(iconBytes))) +} + +// SetTitle sets the systray title, only available on Mac. +func SetTitle(title string) { + C.setTitle(C.CString(title)) +} + +// SetTooltip sets the systray tooltip to display on mouse hover of the tray icon, +// only available on Mac and Windows. +func SetTooltip(tooltip string) { + C.setTooltip(C.CString(tooltip)) +} + +func addOrUpdateMenuItem(item *MenuItem) { + var disabled C.short + if item.disabled { + disabled = 1 + } + var checked C.short + if item.checked { + checked = 1 + } + C.add_or_update_menu_item( + C.int(item.id), + C.CString(item.title), + C.CString(item.tooltip), + disabled, + checked, + ) +} + +// 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)) +} + +func hideMenuItem(item *MenuItem) { + C.hide_menu_item( + C.int(item.id), + ) +} + +func showMenuItem(item *MenuItem) { + C.show_menu_item( + C.int(item.id), + ) +} + +//export systray_ready +func systray_ready() { + systrayReady() +} + +//export systray_on_exit +func systray_on_exit() { + systrayExit() +} + +//export systray_menu_item_selected +func systray_menu_item_selected(cID C.int) { + systrayMenuItemSelected(int32(cID)) +} diff --git a/vendor/github.com/getlantern/systray/systray_windows.go b/vendor/github.com/getlantern/systray/systray_windows.go new file mode 100644 index 0000000..9d560b7 --- /dev/null +++ b/vendor/github.com/getlantern/systray/systray_windows.go @@ -0,0 +1,718 @@ +// +build windows + +package systray + +import ( + "crypto/md5" + "encoding/hex" + "io/ioutil" + "os" + "path/filepath" + "sort" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +// 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") +) + +// 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 + } + 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 + } + 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 + } + 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 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, + 64, + 64, + LR_LOADFROMFILE, + ) + if res == 0 { + return 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) + } + 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)) + + res, _, err := pInsertMenuItem.Call( + uintptr(t.menu), + uintptr(menuId), + 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))) + } + } +} + +func quit() { + const WM_CLOSE = 0x0010 + + pPostMessage.Call( + uintptr(wt.window), + WM_CLOSE, + 0, + 0, + ) +} + +// 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 + } + } + + if err := wt.setIcon(iconFilePath); err != nil { + log.Errorf("Unable to set icon: %v", err) + return + } +} + +// 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 +} + +// 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) + return + } +} + +func addOrUpdateMenuItem(item *MenuItem) { + err := wt.addOrUpdateMenuItem(item.id, item.title, item.disabled, item.checked) + if err != nil { + log.Errorf("Unable to addOrUpdateMenuItem: %v", err) + return + } +} + +func addSeparator(id int32) { + err := wt.addSeparatorMenuItem(id) + if err != nil { + log.Errorf("Unable to addSeparator: %v", err) + return + } +} + +func hideMenuItem(item *MenuItem) { + err := wt.hideMenuItem(item.id) + if err != nil { + log.Errorf("Unable to hideMenuItem: %v", err) + return + } +} + +func showMenuItem(item *MenuItem) { + addOrUpdateMenuItem(item) +} diff --git a/vendor/github.com/getlantern/systray/systray_windows_test.go b/vendor/github.com/getlantern/systray/systray_windows_test.go new file mode 100644 index 0000000..7cb6c75 --- /dev/null +++ b/vendor/github.com/getlantern/systray/systray_windows_test.go @@ -0,0 +1,132 @@ +// +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) +} |