From e79abb69ebedd7275cb6f8e169386c7bc9ad5829 Mon Sep 17 00:00:00 2001 From: Nazar Kanaev Date: Wed, 17 Mar 2021 00:27:18 +0000 Subject: [PATCH] include systray --- go.mod | 2 +- go.sum | 22 +- src/platform/gui.go | 2 +- src/systray/.gitignore | 12 + src/systray/CHANGELOG.md | 125 ++++ src/systray/LICENSE | 202 ++++++ src/systray/Makefile | 18 + src/systray/NOTES.txt | 11 + src/systray/README.md | 120 ++++ src/systray/systray.go | 233 +++++++ src/systray/systray.h | 17 + src/systray/systray_darwin.go | 38 ++ src/systray/systray_darwin.m | 293 +++++++++ src/systray/systray_linux.c | 268 ++++++++ src/systray/systray_linux.go | 29 + src/systray/systray_nonwindows.go | 106 ++++ src/systray/systray_windows.go | 939 ++++++++++++++++++++++++++++ src/systray/systray_windows_test.go | 132 ++++ 18 files changed, 2547 insertions(+), 22 deletions(-) create mode 100644 src/systray/.gitignore create mode 100644 src/systray/CHANGELOG.md create mode 100644 src/systray/LICENSE create mode 100644 src/systray/Makefile create mode 100644 src/systray/NOTES.txt create mode 100644 src/systray/README.md create mode 100644 src/systray/systray.go create mode 100644 src/systray/systray.h create mode 100644 src/systray/systray_darwin.go create mode 100644 src/systray/systray_darwin.m create mode 100644 src/systray/systray_linux.c create mode 100644 src/systray/systray_linux.go create mode 100644 src/systray/systray_nonwindows.go create mode 100644 src/systray/systray_windows.go create mode 100644 src/systray/systray_windows_test.go diff --git a/go.mod b/go.mod index 01a1b6d..f4ef7ff 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,10 @@ go 1.16 require ( github.com/PuerkitoBio/goquery v1.5.1 - github.com/getlantern/systray v1.0.4 github.com/mattn/go-sqlite3 v1.14.0 github.com/mmcdole/gofeed v1.0.0 golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e + golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 ) replace github.com/mmcdole/gofeed => ./src/gofeed diff --git a/go.sum b/go.sum index d0f9f6b..8faeedc 100644 --- a/go.sum +++ b/go.sum @@ -7,22 +7,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/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/systray v1.0.4 h1:qJ/bOlYhn5nsj2FejutWWVFMbhOkYhsChoy26OjgZgU= -github.com/getlantern/systray v1.0.4/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM= -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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -34,8 +18,6 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= -github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -52,8 +34,8 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7 golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 h1:YTzHMGlqJu67/uEo1lBv0n3wBXhXNeUbB1XfN2vmTm0= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 h1:5jaG59Zhd+8ZXe8C+lgiAGqkOaZBruqrWclLkgAww34= +golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= diff --git a/src/platform/gui.go b/src/platform/gui.go index cd9e4e1..bc1308c 100644 --- a/src/platform/gui.go +++ b/src/platform/gui.go @@ -3,7 +3,7 @@ package platform import ( - "github.com/getlantern/systray" + "github.com/nkanaev/yarr/src/systray" "github.com/nkanaev/yarr/src/server" ) diff --git a/src/systray/.gitignore b/src/systray/.gitignore new file mode 100644 index 0000000..2e85ef6 --- /dev/null +++ b/src/systray/.gitignore @@ -0,0 +1,12 @@ +example/example +webview_example/webview_example +*~ +*.swp +**/*.exe +Release +Debug +*.sdf +dll/systray_unsigned.dll +out.txt +.vs +on_exit*.txt diff --git a/src/systray/CHANGELOG.md b/src/systray/CHANGELOG.md new file mode 100644 index 0000000..58e7fc8 --- /dev/null +++ b/src/systray/CHANGELOG.md @@ -0,0 +1,125 @@ +# Changelog + +## [v1.1.0](https://github.com/getlantern/systray/tree/v1.1.0) (2020-11-18) + +[Full Changelog](https://github.com/getlantern/systray/compare/v1.0.5...v1.1.0) + +**Merged pull requests:** + +- Add submenu support for Linux [\#183](https://github.com/getlantern/systray/pull/183) ([fbrinker](https://github.com/fbrinker)) +- Add checkbox support for Linux [\#181](https://github.com/getlantern/systray/pull/181) ([fbrinker](https://github.com/fbrinker)) +- fix SetTitle documentation [\#179](https://github.com/getlantern/systray/pull/179) ([delthas](https://github.com/delthas)) + +## [v1.0.5](https://github.com/getlantern/systray/tree/v1.0.5) (2020-10-19) + +[Full Changelog](https://github.com/getlantern/systray/compare/v1.0.4...v1.0.5) + +**Merged pull requests:** + +- start menu ID with positive, and change the type to uint32 [\#173](https://github.com/getlantern/systray/pull/173) ([joesis](https://github.com/joesis)) +- Allows disabling items in submenu on macOS [\#172](https://github.com/getlantern/systray/pull/172) ([joesis](https://github.com/joesis)) +- Does not use the template icon for regular icons [\#171](https://github.com/getlantern/systray/pull/171) ([sithembiso](https://github.com/sithembiso)) + +## [v1.0.4](https://github.com/getlantern/systray/tree/v1.0.4) (2020-07-21) + +[Full Changelog](https://github.com/getlantern/systray/compare/1.0.3...v1.0.4) + +**Merged pull requests:** + +- protect shared data structures with proper mutexes [\#162](https://github.com/getlantern/systray/pull/162) ([joesis](https://github.com/joesis)) + +## [1.0.3](https://github.com/getlantern/systray/tree/1.0.3) (2020-06-11) + +[Full Changelog](https://github.com/getlantern/systray/compare/v1.0.3...1.0.3) + +## [v1.0.3](https://github.com/getlantern/systray/tree/v1.0.3) (2020-06-11) + +[Full Changelog](https://github.com/getlantern/systray/compare/v1.0.2...v1.0.3) + +**Merged pull requests:** + +- have a workaround to avoid crash on old macOS versions [\#153](https://github.com/getlantern/systray/pull/153) ([joesis](https://github.com/joesis)) +- Fix bug on darwin of setting icon for menu [\#147](https://github.com/getlantern/systray/pull/147) ([mangalaman93](https://github.com/mangalaman93)) + +## [v1.0.2](https://github.com/getlantern/systray/tree/v1.0.2) (2020-05-19) + +[Full Changelog](https://github.com/getlantern/systray/compare/v1.0.1...v1.0.2) + +**Merged pull requests:** + +- remove unused dependencies [\#145](https://github.com/getlantern/systray/pull/145) ([joesis](https://github.com/joesis)) + +## [v1.0.1](https://github.com/getlantern/systray/tree/v1.0.1) (2020-05-18) + +[Full Changelog](https://github.com/getlantern/systray/compare/1.0.1...v1.0.1) + +## [1.0.1](https://github.com/getlantern/systray/tree/1.0.1) (2020-05-18) + +[Full Changelog](https://github.com/getlantern/systray/compare/1.0.0...1.0.1) + +**Merged pull requests:** + +- Unlock menuItemsLock before changing UI [\#144](https://github.com/getlantern/systray/pull/144) ([joesis](https://github.com/joesis)) + +## [1.0.0](https://github.com/getlantern/systray/tree/1.0.0) (2020-05-18) + +[Full Changelog](https://github.com/getlantern/systray/compare/v1.0.0...1.0.0) + +## [v1.0.0](https://github.com/getlantern/systray/tree/v1.0.0) (2020-05-18) + +[Full Changelog](https://github.com/getlantern/systray/compare/0.9.0...v1.0.0) + +**Merged pull requests:** + +- Check if the menu item is nil [\#137](https://github.com/getlantern/systray/pull/137) ([myleshorton](https://github.com/myleshorton)) + +## [0.9.0](https://github.com/getlantern/systray/tree/0.9.0) (2020-03-24) + +[Full Changelog](https://github.com/getlantern/systray/compare/v0.9.0...0.9.0) + +## [v0.9.0](https://github.com/getlantern/systray/tree/v0.9.0) (2020-03-24) + +[Full Changelog](https://github.com/getlantern/systray/compare/8e63b37ef27d94f6db79c4ffb941608e8f0dc2f9...v0.9.0) + +**Merged pull requests:** + +- Backport all features and fixes from master [\#140](https://github.com/getlantern/systray/pull/140) ([joesis](https://github.com/joesis)) +- Nested menu windows [\#132](https://github.com/getlantern/systray/pull/132) ([joesis](https://github.com/joesis)) +- Support for nested sub-menus on OS X [\#131](https://github.com/getlantern/systray/pull/131) ([oxtoacart](https://github.com/oxtoacart)) +- Use temp directory for walk resource manager [\#129](https://github.com/getlantern/systray/pull/129) ([max-b](https://github.com/max-b)) +- Added support for template icons on macOS [\#119](https://github.com/getlantern/systray/pull/119) ([oxtoacart](https://github.com/oxtoacart)) +- When launching app window on macOS, make application a foreground app… [\#118](https://github.com/getlantern/systray/pull/118) ([oxtoacart](https://github.com/oxtoacart)) +- Include stdlib.h in systray\_browser\_linux to explicitly declare funct… [\#114](https://github.com/getlantern/systray/pull/114) ([oxtoacart](https://github.com/oxtoacart)) +- Fix panic when resources root path is not the working directory [\#112](https://github.com/getlantern/systray/pull/112) ([ksubileau](https://github.com/ksubileau)) +- Don't print close reason to console [\#111](https://github.com/getlantern/systray/pull/111) ([ksubileau](https://github.com/ksubileau)) +- Systray icon could not be changed dynamically [\#110](https://github.com/getlantern/systray/pull/110) ([ksubileau](https://github.com/ksubileau)) +- Preventing deadlock on menu item ClickeCh when no one is listening, c… [\#105](https://github.com/getlantern/systray/pull/105) ([oxtoacart](https://github.com/oxtoacart)) +- Reverted deadlock fix \(Affected other receivers\) [\#104](https://github.com/getlantern/systray/pull/104) ([ldstein](https://github.com/ldstein)) +- Fix Deadlock and item ordering in Windows [\#103](https://github.com/getlantern/systray/pull/103) ([ldstein](https://github.com/ldstein)) +- Minor README improvements \(go modules, example app, screenshot\) [\#98](https://github.com/getlantern/systray/pull/98) ([tstromberg](https://github.com/tstromberg)) +- Add support for app window [\#97](https://github.com/getlantern/systray/pull/97) ([oxtoacart](https://github.com/oxtoacart)) +- systray\_darwin.m: Compare Mac OS min version with value instead of macro [\#94](https://github.com/getlantern/systray/pull/94) ([teddywing](https://github.com/teddywing)) +- Attempt to fix https://github.com/getlantern/systray/issues/75 [\#92](https://github.com/getlantern/systray/pull/92) ([mikeschinkel](https://github.com/mikeschinkel)) +- Fix application path for MacOS in README [\#91](https://github.com/getlantern/systray/pull/91) ([zereraz](https://github.com/zereraz)) +- Document cross-platform console window details [\#81](https://github.com/getlantern/systray/pull/81) ([michaelsanford](https://github.com/michaelsanford)) +- Fix bad-looking system tray icon in Windows [\#78](https://github.com/getlantern/systray/pull/78) ([juja256](https://github.com/juja256)) +- Add the separator to the visible items [\#76](https://github.com/getlantern/systray/pull/76) ([meskio](https://github.com/meskio)) +- keep track of hidden items [\#74](https://github.com/getlantern/systray/pull/74) ([kalikaneko](https://github.com/kalikaneko)) +- Support macOS older than 10.13 [\#73](https://github.com/getlantern/systray/pull/73) ([swznd](https://github.com/swznd)) +- define ERROR\_SUCCESS as syscall.Errno [\#69](https://github.com/getlantern/systray/pull/69) ([joesis](https://github.com/joesis)) +- Bug/fix broken menuitem show [\#68](https://github.com/getlantern/systray/pull/68) ([kalikaneko](https://github.com/kalikaneko)) +- Fix mac deprecations [\#66](https://github.com/getlantern/systray/pull/66) ([jefvel](https://github.com/jefvel)) +- Made it possible to add icons to menu items on Mac [\#65](https://github.com/getlantern/systray/pull/65) ([jefvel](https://github.com/jefvel)) +- linux: delete temp files as soon as they are not needed [\#63](https://github.com/getlantern/systray/pull/63) ([meskio](https://github.com/meskio)) +- Merge changes from amkulikov to remove DLL for windows [\#56](https://github.com/getlantern/systray/pull/56) ([oxtoacart](https://github.com/oxtoacart)) +- Revert "Use templated icons for the menu bar in macOS" [\#51](https://github.com/getlantern/systray/pull/51) ([stoggi](https://github.com/stoggi)) +- Use templated icons for the menu bar in macOS [\#46](https://github.com/getlantern/systray/pull/46) ([stoggi](https://github.com/stoggi)) +- Syscalls instead of custom DLLs [\#44](https://github.com/getlantern/systray/pull/44) ([amkulikov](https://github.com/amkulikov)) +- On quit exit main loop on linux [\#41](https://github.com/getlantern/systray/pull/41) ([meskio](https://github.com/meskio)) +- Fixed hide show in linux \(\#37\) [\#39](https://github.com/getlantern/systray/pull/39) ([meskio](https://github.com/meskio)) +- fix: linux compilation warning [\#36](https://github.com/getlantern/systray/pull/36) ([novln](https://github.com/novln)) +- Added separator functionality [\#32](https://github.com/getlantern/systray/pull/32) ([oxtoacart](https://github.com/oxtoacart)) + + + +\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/src/systray/LICENSE b/src/systray/LICENSE new file mode 100644 index 0000000..3ee0162 --- /dev/null +++ b/src/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/src/systray/Makefile b/src/systray/Makefile new file mode 100644 index 0000000..12f3d22 --- /dev/null +++ b/src/systray/Makefile @@ -0,0 +1,18 @@ +tag-changelog: require-version require-gh-token + echo "Tagging..." && \ + git tag -a "$$VERSION" -f --annotate -m"Tagged $$VERSION" && \ + git push --tags -f && \ + git checkout master && \ + git pull && \ + github_changelog_generator --no-issues --max-issues 100 --token "${GH_TOKEN}" --user getlantern --project systray && \ + git add CHANGELOG.md && \ + git commit -m "Updated changelog for $$VERSION" && \ + git push origin HEAD && \ + git checkout - + +guard-%: + @ if [ -z '${${*}}' ]; then echo 'Environment variable $* not set' && exit 1; fi + +require-version: guard-VERSION + +require-gh-token: guard-GH_TOKEN diff --git a/src/systray/NOTES.txt b/src/systray/NOTES.txt new file mode 100644 index 0000000..3bd6f2e --- /dev/null +++ b/src/systray/NOTES.txt @@ -0,0 +1,11 @@ +taken from: + +repo: +https://github.com/getlantern/systray + +hash: +2c0986dda9aea361e925f90e848d9036be7b5367 + +changes: + +-removed `getlantern/golog` dependency diff --git a/src/systray/README.md b/src/systray/README.md new file mode 100644 index 0000000..543acfd --- /dev/null +++ b/src/systray/README.md @@ -0,0 +1,120 @@ +systray is a cross-platform Go library to place an icon and menu in the notification area. + +## Features + +* Supported on Windows, macOS, and Linux +* Menu items can be checked and/or disabled +* Methods may be called from any Goroutine + +## API + +```go +func 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 and Windows. + mQuit.SetIcon(icon.Data) +} + +func onExit() { + // clean up here +} +``` + +See [full API](https://pkg.go.dev/github.com/getlantern/systray?tab=doc) as well as [CHANGELOG](https://github.com/getlantern/systray/tree/master/CHANGELOG.md). + +## Try the example app! + +Have go v1.12+ or higher installed? Here's an example to get started on macOS: + +```sh +git clone https://github.com/getlantern/systray +cd example +env GO111MODULE=on go build +./example +``` + +On Windows, you should build like this: + +``` +env GO111MODULE=on go build -ldflags "-H=windowsgui" +``` + +The following text will then appear on the console: + + +```sh +go: finding github.com/skratchdot/open-golang latest +go: finding github.com/getlantern/systray latest +go: finding github.com/getlantern/golog latest +``` + +Now look for *Awesome App* in your menu bar! + +![Awesome App screenshot](example/screenshot.png) + +## The Webview example + +The code under `webview_example` is to demostrate how it can co-exist with other UI elements. Note that the example doesn't work on macOS versions older than 10.15 Catalina. + +## Platform notes + +### Linux + +* Building apps requires gcc as well as the `gtk3` and `libappindicator3` development headers to be installed. For Debian or Ubuntu, you may install these using: + +```sh +sudo apt-get install gcc libgtk-3-dev libappindicator3-dev +``` + +On Linux Mint, `libxapp-dev` is also required . + +To build `webview_example`, you also need to install `libwebkit2gtk-4.0-dev` and remove `webview_example/rsrc.syso` which is required on Windows. + +### Windows + +* To avoid opening a console at application startup, use these compile flags: + +```sh +go build -ldflags -H=windowsgui +``` + +### macOS + +On macOS, you will need to create an application bundle to wrap the binary; simply folders with the following minimal structure and assets: + +``` +SystrayApp.app/ + Contents/ + Info.plist + MacOS/ + go-executable + Resources/ + SystrayApp.icns +``` + +When running as an app bundle, you may want to add one or both of the following to your Info.plist: + +```xml + + NSHighResolutionCapable + True + + + LSUIElement + 1 +``` + +Consult the [Official Apple Documentation here](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW1). + +## Credits + +- https://github.com/xilp/systray +- https://github.com/cratonica/trayhost diff --git a/src/systray/systray.go b/src/systray/systray.go new file mode 100644 index 0000000..086c6c2 --- /dev/null +++ b/src/systray/systray.go @@ -0,0 +1,233 @@ +/* +Package systray is a cross-platform Go library to place an icon and menu in the notification area. +*/ +package systray + +import ( + "fmt" + "log" + "runtime" + "sync" + "sync/atomic" +) + +var ( + systrayReady func() + systrayExit func() + menuItems = make(map[uint32]*MenuItem) + menuItemsLock sync.RWMutex + + currentID = uint32(0) + quitOnce sync.Once +) + +func init() { + runtime.LockOSThread() +} + +// MenuItem is used to keep track each menu item of systray. +// Don't create it directly, use the one systray.AddMenuItem() returned +type MenuItem struct { + // ClickedCh is the channel which will be notified when the menu item is clicked + ClickedCh chan struct{} + + // id uniquely identify a menu item, not supposed to be modified + id uint32 + // 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 + // has the menu item a checkbox (Linux) + isCheckable bool + // parent item, for sub menus + parent *MenuItem +} + +func (item *MenuItem) String() string { + if item.parent == nil { + return fmt.Sprintf("MenuItem[%d, %q]", item.id, item.title) + } + return fmt.Sprintf("MenuItem[%d, parent %d, %q]", item.id, item.parent.id, item.title) +} + +// newMenuItem returns a populated MenuItem object +func newMenuItem(title string, tooltip string, parent *MenuItem) *MenuItem { + return &MenuItem{ + ClickedCh: make(chan struct{}), + id: atomic.AddUint32(¤tID, 1), + title: title, + tooltip: tooltip, + disabled: false, + checked: false, + isCheckable: false, + parent: parent, + } +} + +// Run initializes GUI and starts the event loop, then invokes the onReady +// callback. It blocks until systray.Quit() is called. +func Run(onReady func(), onExit func()) { + Register(onReady, onExit) + nativeLoop() +} + +// Register initializes GUI and registers the callbacks but relies on the +// caller to run the event loop somewhere else. It's useful if the program +// needs to show other UI elements, for example, webview. +// To overcome some OS weirdness, On macOS versions before Catalina, calling +// this does exactly the same as Run(). +func Register(onReady func(), onExit func()) { + 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 + registerSystray() +} + +// Quit the systray +func Quit() { + quitOnce.Do(quit) +} + +// AddMenuItem adds a menu item with the designated title and tooltip. +// It can be safely invoked from different goroutines. +// Created menu items are checkable on Windows and OSX by default. For Linux you have to use AddMenuItemCheckbox +func AddMenuItem(title string, tooltip string) *MenuItem { + item := newMenuItem(title, tooltip, nil) + item.update() + return item +} + +// AddMenuItemCheckbox adds a menu item with the designated title and tooltip and a checkbox for Linux. +// It can be safely invoked from different goroutines. +// On Windows and OSX this is the same as calling AddMenuItem +func AddMenuItemCheckbox(title string, tooltip string, checked bool) *MenuItem { + item := newMenuItem(title, tooltip, nil) + item.isCheckable = true + item.checked = checked + item.update() + return item +} + +// AddSeparator adds a separator bar to the menu +func AddSeparator() { + addSeparator(atomic.AddUint32(¤tID, 1)) +} + +// AddSubMenuItem adds a nested sub-menu item with the designated title and tooltip. +// It can be safely invoked from different goroutines. +// Created menu items are checkable on Windows and OSX by default. For Linux you have to use AddSubMenuItemCheckbox +func (item *MenuItem) AddSubMenuItem(title string, tooltip string) *MenuItem { + child := newMenuItem(title, tooltip, item) + child.update() + return child +} + +// AddSubMenuItemCheckbox adds a nested sub-menu item with the designated title and tooltip and a checkbox for Linux. +// It can be safely invoked from different goroutines. +// On Windows and OSX this is the same as calling AddSubMenuItem +func (item *MenuItem) AddSubMenuItemCheckbox(title string, tooltip string, checked bool) *MenuItem { + child := newMenuItem(title, tooltip, item) + child.isCheckable = true + child.checked = checked + child.update() + return child +} + +// 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 checks 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 propagates changes on a menu item to systray +func (item *MenuItem) update() { + menuItemsLock.Lock() + menuItems[item.id] = item + menuItemsLock.Unlock() + addOrUpdateMenuItem(item) +} + +func systrayMenuItemSelected(id uint32) { + menuItemsLock.RLock() + item, ok := menuItems[id] + menuItemsLock.RUnlock() + if !ok { + log.Printf("No menu item with ID %v", id) + return + } + select { + case item.ClickedCh <- struct{}{}: + // in case no one waiting for the channel + default: + } +} diff --git a/src/systray/systray.h b/src/systray/systray.h new file mode 100644 index 0000000..888c829 --- /dev/null +++ b/src/systray/systray.h @@ -0,0 +1,17 @@ +#include "stdbool.h" + +extern void systray_ready(); +extern void systray_on_exit(); +extern void systray_menu_item_selected(int menu_id); +void registerSystray(void); +int nativeLoop(void); + +void setIcon(const char* iconBytes, int length, bool template); +void setMenuItemIcon(const char* iconBytes, int length, int menuId, bool template); +void setTitle(char* title); +void setTooltip(char* tooltip); +void add_or_update_menu_item(int menuId, int parentMenuId, char* title, char* tooltip, short disabled, short checked, short isCheckable); +void add_separator(int menuId); +void hide_menu_item(int menuId); +void show_menu_item(int menuId); +void quit(); diff --git a/src/systray/systray_darwin.go b/src/systray/systray_darwin.go new file mode 100644 index 0000000..740ec5b --- /dev/null +++ b/src/systray/systray_darwin.go @@ -0,0 +1,38 @@ +package systray + +/* +#cgo darwin CFLAGS: -DDARWIN -x objective-c -fobjc-arc +#cgo darwin LDFLAGS: -framework Cocoa -framework WebKit + +#include "systray.h" +*/ +import "C" + +import ( + "unsafe" +) + +// SetTemplateIcon sets the systray icon as a template icon (on Mac), falling back +// to a regular icon on other platforms. +// templateIconBytes and regularIconBytes should be the content of .ico for windows and +// .ico/.jpg/.png for other platforms. +func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { + cstr := (*C.char)(unsafe.Pointer(&templateIconBytes[0])) + C.setIcon(cstr, (C.int)(len(templateIconBytes)), true) +} + +// SetIcon sets the icon of a menu item. Only works on macOS and Windows. +// iconBytes should be the content of .ico/.jpg/.png +func (item *MenuItem) SetIcon(iconBytes []byte) { + cstr := (*C.char)(unsafe.Pointer(&iconBytes[0])) + C.setMenuItemIcon(cstr, (C.int)(len(iconBytes)), C.int(item.id), false) +} + +// SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows, it +// falls back to the regular icon bytes and on Linux it does nothing. +// templateIconBytes and regularIconBytes should be the content of .ico for windows and +// .ico/.jpg/.png for other platforms. +func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { + cstr := (*C.char)(unsafe.Pointer(&templateIconBytes[0])) + C.setMenuItemIcon(cstr, (C.int)(len(templateIconBytes)), C.int(item.id), true) +} diff --git a/src/systray/systray_darwin.m b/src/systray/systray_darwin.m new file mode 100644 index 0000000..884fa43 --- /dev/null +++ b/src/systray/systray_darwin.m @@ -0,0 +1,293 @@ +#import +#include "systray.h" + +#if __MAC_OS_X_VERSION_MIN_REQUIRED < 101400 + + #ifndef NSControlStateValueOff + #define NSControlStateValueOff NSOffState + #endif + + #ifndef NSControlStateValueOn + #define NSControlStateValueOn NSOnState + #endif + +#endif + +@interface MenuItem : NSObject +{ + @public + NSNumber* menuId; + NSNumber* parentMenuId; + NSString* title; + NSString* tooltip; + short disabled; + short checked; +} +-(id) initWithId: (int)theMenuId +withParentMenuId: (int)theParentMenuId + withTitle: (const char*)theTitle + withTooltip: (const char*)theTooltip + withDisabled: (short)theDisabled + withChecked: (short)theChecked; + @end + @implementation MenuItem + -(id) initWithId: (int)theMenuId + withParentMenuId: (int)theParentMenuId + withTitle: (const char*)theTitle + withTooltip: (const char*)theTooltip + withDisabled: (short)theDisabled + withChecked: (short)theChecked +{ + menuId = [NSNumber numberWithInt:theMenuId]; + parentMenuId = [NSNumber numberWithInt:theParentMenuId]; + title = [[NSString alloc] initWithCString:theTitle + encoding:NSUTF8StringEncoding]; + tooltip = [[NSString alloc] initWithCString:theTooltip + encoding:NSUTF8StringEncoding]; + disabled = theDisabled; + checked = theChecked; + return self; +} +@end + +@interface AppDelegate: NSObject + - (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 { + NSMenu *theMenu = self->menu; + NSMenuItem *parentItem; + if ([item->parentMenuId integerValue] > 0) { + parentItem = find_menu_item(menu, item->parentMenuId); + if (parentItem.hasSubmenu) { + theMenu = parentItem.submenu; + } else { + theMenu = [[NSMenu alloc] init]; + [theMenu setAutoenablesItems:NO]; + [parentItem setSubmenu:theMenu]; + } + } + + NSMenuItem *menuItem; + menuItem = find_menu_item(theMenu, item->menuId); + if (menuItem == NULL) { + menuItem = [theMenu addItemWithTitle:item->title + action:@selector(menuHandler:) + keyEquivalent:@""]; + [menuItem setRepresentedObject:item->menuId]; + } + [menuItem setTitle:item->title]; + [menuItem setTag:[item->menuId integerValue]]; + [menuItem setTarget:self]; + [menuItem setToolTip:item->tooltip]; + if (item->disabled == 1) { + menuItem.enabled = FALSE; + } else { + menuItem.enabled = TRUE; + } + if (item->checked == 1) { + menuItem.state = NSControlStateValueOn; + } else { + menuItem.state = NSControlStateValueOff; + } +} + +NSMenuItem *find_menu_item(NSMenu *ourMenu, NSNumber *menuId) { + NSMenuItem *foundItem = [ourMenu itemWithTag:[menuId integerValue]]; + if (foundItem != NULL) { + return foundItem; + } + NSArray *menu_items = ourMenu.itemArray; + int i; + for (i = 0; i < [menu_items count]; i++) { + NSMenuItem *i_item = [menu_items objectAtIndex:i]; + if (i_item.hasSubmenu) { + foundItem = find_menu_item(i_item.submenu, menuId); + if (foundItem != NULL) { + return foundItem; + } + } + } + + return NULL; +}; + +- (void) add_separator:(NSNumber*) menuId +{ + [menu addItem: [NSMenuItem separatorItem]]; +} + +- (void) hide_menu_item:(NSNumber*) menuId +{ + NSMenuItem* menuItem = find_menu_item(menu, menuId); + if (menuItem != NULL) { + [menuItem setHidden:TRUE]; + } +} + +- (void) setMenuItemIcon:(NSArray*)imageAndMenuId { + NSImage* image = [imageAndMenuId objectAtIndex:0]; + NSNumber* menuId = [imageAndMenuId objectAtIndex:1]; + + NSMenuItem* menuItem; + menuItem = find_menu_item(menu, menuId); + if (menuItem == NULL) { + return; + } + menuItem.image = image; +} + +- (void) show_menu_item:(NSNumber*) menuId +{ + NSMenuItem* menuItem = find_menu_item(menu, menuId); + if (menuItem != NULL) { + [menuItem setHidden:FALSE]; + } +} + +- (void) quit +{ + [NSApp terminate:self]; +} + +@end + +void registerSystray(void) { + AppDelegate *delegate = [[AppDelegate alloc] init]; + [[NSApplication sharedApplication] setDelegate:delegate]; + // A workaround to avoid crashing on macOS versions before Catalina. Somehow + // SIGSEGV would happen inside AppKit if [NSApp run] is called from a + // different function, even if that function is called right after this. + if (floor(NSAppKitVersionNumber) <= /*NSAppKitVersionNumber10_14*/ 1671){ + [NSApp run]; + } +} + +int nativeLoop(void) { + if (floor(NSAppKitVersionNumber) > /*NSAppKitVersionNumber10_14*/ 1671){ + [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, bool template) { + NSData* buffer = [NSData dataWithBytes: iconBytes length:length]; + NSImage *image = [[NSImage alloc] initWithData:buffer]; + [image setSize:NSMakeSize(16, 16)]; + image.template = template; + runInMainThread(@selector(setIcon:), (id)image); +} + +void setMenuItemIcon(const char* iconBytes, int length, int menuId, bool template) { + NSData* buffer = [NSData dataWithBytes: iconBytes length:length]; + NSImage *image = [[NSImage alloc] initWithData:buffer]; + [image setSize:NSMakeSize(16, 16)]; + image.template = template; + NSNumber *mId = [NSNumber numberWithInt:menuId]; + runInMainThread(@selector(setMenuItemIcon:), @[image, (id)mId]); +} + +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, int parentMenuId, char* title, char* tooltip, short disabled, short checked, short isCheckable) { + MenuItem* item = [[MenuItem alloc] initWithId: menuId withParentMenuId: parentMenuId withTitle: title withTooltip: tooltip withDisabled: disabled withChecked: checked]; + free(title); + free(tooltip); + runInMainThread(@selector(add_or_update_menu_item:), (id)item); +} + +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/src/systray/systray_linux.c b/src/systray/systray_linux.c new file mode 100644 index 0000000..8ec3e76 --- /dev/null +++ b/src/systray/systray_linux.c @@ -0,0 +1,268 @@ +#include +#include +#include +#include +#include +#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; + long signalHandlerId; +} MenuItemNode; + +typedef struct { + int menu_id; + int parent_menu_id; + char* title; + char* tooltip; + short disabled; + short checked; + short isCheckable; +} MenuItemInfo; + +void registerSystray(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(); +} + +int nativeLoop(void) { + 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); +} + +GtkMenuItem* find_menu_by_id(int id) { + GList* it; + for(it = global_menu_items; it != NULL; it = it->next) { + MenuItemNode* item = (MenuItemNode*)(it->data); + if(item->menu_id == id) { + return GTK_MENU_ITEM(item->menu_item); + } + } + return NULL; +} + +// 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); + + if (mii->isCheckable) { + // We need to block the "activate" event, to emulate the same behaviour as in the windows version + // A Check/Uncheck does change the checkbox, but does not trigger the checkbox menuItem channel + g_signal_handler_block(GTK_CHECK_MENU_ITEM(item->menu_item), item->signalHandlerId); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item->menu_item), mii->checked == 1); + g_signal_handler_unblock(GTK_CHECK_MENU_ITEM(item->menu_item), item->signalHandlerId); + } + break; + } + } + + // menu id doesn't exist, add new item + if(it == NULL) { + GtkWidget *menu_item; + if (mii->isCheckable) { + menu_item = gtk_check_menu_item_new_with_label(mii->title); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menu_item), mii->checked == 1); + } else { + menu_item = gtk_menu_item_new_with_label(mii->title); + } + int *id = malloc(sizeof(int)); + *id = mii->menu_id; + long signalHandlerId = g_signal_connect_swapped( + G_OBJECT(menu_item), + "activate", + G_CALLBACK(_systray_menu_item_selected), + id + ); + + if (mii->parent_menu_id == 0) { + gtk_menu_shell_append(GTK_MENU_SHELL(global_tray_menu), menu_item); + } else { + GtkMenuItem* parentMenuItem = find_menu_by_id(mii->parent_menu_id); + GtkWidget* parentMenu = gtk_menu_item_get_submenu(parentMenuItem); + + if(parentMenu == NULL) { + parentMenu = gtk_menu_new(); + gtk_menu_item_set_submenu(parentMenuItem, parentMenu); + } + + gtk_menu_shell_append(GTK_MENU_SHELL(parentMenu), menu_item); + } + + MenuItemNode* new_item = malloc(sizeof(MenuItemNode)); + new_item->menu_id = mii->menu_id; + new_item->signalHandlerId = signalHandlerId; + 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); + 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, bool template) { + 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, bool template) { +} + +void add_or_update_menu_item(int menu_id, int parent_menu_id, char* title, char* tooltip, short disabled, short checked, short isCheckable) { + MenuItemInfo *mii = malloc(sizeof(MenuItemInfo)); + mii->menu_id = menu_id; + mii->parent_menu_id = parent_menu_id; + mii->title = title; + mii->tooltip = tooltip; + mii->disabled = disabled; + mii->checked = checked; + mii->isCheckable = isCheckable; + 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/src/systray/systray_linux.go b/src/systray/systray_linux.go new file mode 100644 index 0000000..1f508c7 --- /dev/null +++ b/src/systray/systray_linux.go @@ -0,0 +1,29 @@ +package systray + +/* +#cgo darwin CFLAGS: -DDARWIN -x objective-c -fobjc-arc +#cgo darwin LDFLAGS: -framework Cocoa -framework WebKit + +#include "systray.h" +*/ +import "C" + +// SetTemplateIcon sets the systray icon as a template icon (on macOS), falling back +// to a regular icon on other platforms. +// templateIconBytes and iconBytes should be the content of .ico for windows and +// .ico/.jpg/.png for other platforms. +func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { + SetIcon(regularIconBytes) +} + +// SetIcon sets the icon of a menu item. Only works on macOS and Windows. +// iconBytes should be the content of .ico/.jpg/.png +func (item *MenuItem) SetIcon(iconBytes []byte) { +} + +// SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows, it +// falls back to the regular icon bytes and on Linux it does nothing. +// templateIconBytes and regularIconBytes should be the content of .ico for windows and +// .ico/.jpg/.png for other platforms. +func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { +} diff --git a/src/systray/systray_nonwindows.go b/src/systray/systray_nonwindows.go new file mode 100644 index 0000000..5f6b90b --- /dev/null +++ b/src/systray/systray_nonwindows.go @@ -0,0 +1,106 @@ +// +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 registerSystray() { + C.registerSystray() +} + +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)), false) +} + +// SetTitle sets the systray title, only available on Mac and Linux. +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 + } + var isCheckable C.short + if item.isCheckable { + isCheckable = 1 + } + var parentID uint32 = 0 + if item.parent != nil { + parentID = item.parent.id + } + C.add_or_update_menu_item( + C.int(item.id), + C.int(parentID), + C.CString(item.title), + C.CString(item.tooltip), + disabled, + checked, + isCheckable, + ) +} + +func addSeparator(id uint32) { + 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(uint32(cID)) +} diff --git a/src/systray/systray_windows.go b/src/systray/systray_windows.go new file mode 100644 index 0000000..cf9f23b --- /dev/null +++ b/src/systray/systray_windows.go @@ -0,0 +1,939 @@ +// +build windows + +package systray + +import ( + "crypto/md5" + "encoding/hex" + "io/ioutil" + "os" + "path/filepath" + "sort" + "sync" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +// Helpful sources: https://github.com/golang/exp/blob/master/shiny/driver/internal/win32 + +var ( + g32 = windows.NewLazySystemDLL("Gdi32.dll") + pCreateCompatibleBitmap = g32.NewProc("CreateCompatibleBitmap") + pCreateCompatibleDC = g32.NewProc("CreateCompatibleDC") + pDeleteDC = g32.NewProc("DeleteDC") + pSelectObject = g32.NewProc("SelectObject") + + k32 = windows.NewLazySystemDLL("Kernel32.dll") + pGetModuleHandle = k32.NewProc("GetModuleHandleW") + + s32 = windows.NewLazySystemDLL("Shell32.dll") + pShellNotifyIcon = s32.NewProc("Shell_NotifyIconW") + + u32 = windows.NewLazySystemDLL("User32.dll") + pCreateMenu = u32.NewProc("CreateMenu") + pCreatePopupMenu = u32.NewProc("CreatePopupMenu") + pCreateWindowEx = u32.NewProc("CreateWindowExW") + pDefWindowProc = u32.NewProc("DefWindowProcW") + pDeleteMenu = u32.NewProc("DeleteMenu") + pDestroyWindow = u32.NewProc("DestroyWindow") + pDispatchMessage = u32.NewProc("DispatchMessageW") + pDrawIconEx = u32.NewProc("DrawIconEx") + pGetCursorPos = u32.NewProc("GetCursorPos") + pGetDC = u32.NewProc("GetDC") + pGetMenuItemID = u32.NewProc("GetMenuItemID") + pGetMessage = u32.NewProc("GetMessageW") + pGetSystemMetrics = u32.NewProc("GetSystemMetrics") + pInsertMenuItem = u32.NewProc("InsertMenuItemW") + pLoadCursor = u32.NewProc("LoadCursorW") + pLoadIcon = u32.NewProc("LoadIconW") + pLoadImage = u32.NewProc("LoadImageW") + pPostMessage = u32.NewProc("PostMessageW") + pPostQuitMessage = u32.NewProc("PostQuitMessage") + pRegisterClass = u32.NewProc("RegisterClassExW") + pRegisterWindowMessage = u32.NewProc("RegisterWindowMessageW") + pReleaseDC = u32.NewProc("ReleaseDC") + pSetForegroundWindow = u32.NewProc("SetForegroundWindow") + pSetMenuInfo = u32.NewProc("SetMenuInfo") + pSetMenuItemInfo = u32.NewProc("SetMenuItemInfoW") + 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 + BMPItem 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 windows.Handle + + loadedImages map[string]windows.Handle + muLoadedImages sync.RWMutex + // menus keeps track of the submenus keyed by the menu item ID, plus 0 + // which corresponds to the main popup menu. + menus map[uint32]windows.Handle + muMenus sync.RWMutex + // menuOf keeps track of the menu each menu item belongs to. + menuOf map[uint32]windows.Handle + muMenuOf sync.RWMutex + // menuItemIcons maintains the bitmap of each menu item (if applies). It's + // needed to show the icon correctly when showing a previously hidden menu + // item again. + menuItemIcons map[uint32]windows.Handle + muMenuItemIcons sync.RWMutex + visibleItems map[uint32][]uint32 + muVisibleItems sync.RWMutex + + nid *notifyIconData + muNID sync.RWMutex + wcex *wndClassEx + + wmSystrayMessage, + wmTaskbarCreated uint32 +} + +// Loads an image from file and shows it in tray. +// Shell_NotifyIcon: https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159(v=vs.85).aspx +func (t *winTray) setIcon(src string) error { + const NIF_ICON = 0x00000002 + + h, err := t.loadIconFrom(src) + if err != nil { + return err + } + + t.muNID.Lock() + defer t.muNID.Unlock() + 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 + } + + t.muNID.Lock() + defer t.muNID.Unlock() + 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_RBUTTONUP = 0x0205 + WM_LBUTTONUP = 0x0202 + WM_COMMAND = 0x0111 + WM_ENDSESSION = 0x0016 + WM_CLOSE = 0x0010 + WM_DESTROY = 0x0002 + WM_CREATE = 0x0001 + ) + switch message { + case WM_CREATE: + systrayReady() + case WM_COMMAND: + menuItemId := int32(wParam) + // https://docs.microsoft.com/en-us/windows/win32/menurc/wm-command#menus + if menuItemId != -1 { + systrayMenuItemSelected(uint32(wParam)) + } + case WM_CLOSE: + pDestroyWindow.Call(uintptr(t.window)) + t.wcex.unregister() + case WM_DESTROY: + // same as WM_ENDSESSION, but throws 0 exit code after all + defer pPostQuitMessage.Call(uintptr(int32(0))) + fallthrough + case WM_ENDSESSION: + t.muNID.Lock() + if t.nid != nil { + t.nid.delete() + } + t.muNID.Unlock() + systrayExit() + case t.wmSystrayMessage: + switch lParam { + case WM_RBUTTONUP, WM_LBUTTONUP: + t.showMenu() + } + case t.wmTaskbarCreated: // on explorer.exe restarts + t.muNID.Lock() + t.nid.add() + t.muNID.Unlock() + 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 + t.visibleItems = make(map[uint32][]uint32) + t.menus = make(map[uint32]windows.Handle) + t.menuOf = make(map[uint32]windows.Handle) + t.menuItemIcons = make(map[uint32]windows.Handle) + + taskbarEventNamePtr, _ := windows.UTF16PtrFromString("TaskbarCreated") + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644947 + 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.muNID.Lock() + defer t.muNID.Unlock() + 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.menus[0] = 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.menus[0]), + uintptr(unsafe.Pointer(&mi)), + ) + if res == 0 { + return err + } + return nil +} + +func (t *winTray) convertToSubMenu(menuItemId uint32) (windows.Handle, error) { + const MIIM_SUBMENU = 0x00000004 + + res, _, err := pCreateMenu.Call() + if res == 0 { + return 0, err + } + menu := windows.Handle(res) + + mi := menuItemInfo{Mask: MIIM_SUBMENU, SubMenu: menu} + mi.Size = uint32(unsafe.Sizeof(mi)) + t.muMenuOf.RLock() + hMenu := t.menuOf[menuItemId] + t.muMenuOf.RUnlock() + res, _, err = pSetMenuItemInfo.Call( + uintptr(hMenu), + uintptr(menuItemId), + 0, + uintptr(unsafe.Pointer(&mi)), + ) + if res == 0 { + return 0, err + } + t.muMenus.Lock() + t.menus[menuItemId] = menu + t.muMenus.Unlock() + return menu, nil +} + +func (t *winTray) addOrUpdateMenuItem(menuItemId uint32, parentId uint32, title string, disabled, checked bool) error { + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx + const ( + MIIM_FTYPE = 0x00000100 + MIIM_BITMAP = 0x00000080 + MIIM_STRING = 0x00000040 + MIIM_SUBMENU = 0x00000004 + 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(menuItemId), + TypeData: titlePtr, + Cch: uint32(len(title)), + } + mi.Size = uint32(unsafe.Sizeof(mi)) + if disabled { + mi.State |= MFS_DISABLED + } + if checked { + mi.State |= MFS_CHECKED + } + t.muMenuItemIcons.RLock() + hIcon := t.menuItemIcons[menuItemId] + t.muMenuItemIcons.RUnlock() + if hIcon > 0 { + mi.Mask |= MIIM_BITMAP + mi.BMPItem = hIcon + } + + var res uintptr + t.muMenus.RLock() + menu, exists := t.menus[parentId] + t.muMenus.RUnlock() + if !exists { + menu, err = t.convertToSubMenu(parentId) + if err != nil { + return err + } + t.muMenus.Lock() + t.menus[parentId] = menu + t.muMenus.Unlock() + } else if t.getVisibleItemIndex(parentId, menuItemId) != -1 { + // We set the menu item info based on the menuID + res, _, err = pSetMenuItemInfo.Call( + uintptr(menu), + uintptr(menuItemId), + 0, + uintptr(unsafe.Pointer(&mi)), + ) + } + + if res == 0 { + t.addToVisibleItems(parentId, menuItemId) + position := t.getVisibleItemIndex(parentId, menuItemId) + res, _, err = pInsertMenuItem.Call( + uintptr(menu), + uintptr(position), + 1, + uintptr(unsafe.Pointer(&mi)), + ) + if res == 0 { + t.delFromVisibleItems(parentId, menuItemId) + return err + } + t.muMenuOf.Lock() + t.menuOf[menuItemId] = menu + t.muMenuOf.Unlock() + } + + return nil +} + +func (t *winTray) addSeparatorMenuItem(menuItemId, parentId uint32) error { + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx + const ( + MIIM_FTYPE = 0x00000100 + MIIM_ID = 0x00000002 + MIIM_STATE = 0x00000001 + ) + const MFT_SEPARATOR = 0x00000800 + + mi := menuItemInfo{ + Mask: MIIM_FTYPE | MIIM_ID | MIIM_STATE, + Type: MFT_SEPARATOR, + ID: uint32(menuItemId), + } + + mi.Size = uint32(unsafe.Sizeof(mi)) + + t.addToVisibleItems(parentId, menuItemId) + position := t.getVisibleItemIndex(parentId, menuItemId) + t.muMenus.RLock() + menu := uintptr(t.menus[parentId]) + t.muMenus.RUnlock() + res, _, err := pInsertMenuItem.Call( + menu, + uintptr(position), + 1, + uintptr(unsafe.Pointer(&mi)), + ) + if res == 0 { + return err + } + + return nil +} + +func (t *winTray) hideMenuItem(menuItemId, parentId uint32) error { + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647629(v=vs.85).aspx + const MF_BYCOMMAND = 0x00000000 + const ERROR_SUCCESS syscall.Errno = 0 + + t.muMenus.RLock() + menu := uintptr(t.menus[parentId]) + t.muMenus.RUnlock() + res, _, err := pDeleteMenu.Call( + menu, + uintptr(menuItemId), + MF_BYCOMMAND, + ) + if res == 0 && err.(syscall.Errno) != ERROR_SUCCESS { + return err + } + t.delFromVisibleItems(parentId, menuItemId) + + 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.menus[0]), + 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(parent, val uint32) { + t.muVisibleItems.Lock() + defer t.muVisibleItems.Unlock() + visibleItems := t.visibleItems[parent] + for i, itemval := range visibleItems { + if val == itemval { + visibleItems = append(visibleItems[:i], visibleItems[i+1:]...) + break + } + } +} + +func (t *winTray) addToVisibleItems(parent, val uint32) { + t.muVisibleItems.Lock() + defer t.muVisibleItems.Unlock() + if visibleItems, exists := t.visibleItems[parent]; !exists { + t.visibleItems[parent] = []uint32{val} + } else { + newvisible := append(visibleItems, val) + sort.Slice(newvisible, func(i, j int) bool { return newvisible[i] < newvisible[j] }) + t.visibleItems[parent] = newvisible + } +} + +func (t *winTray) getVisibleItemIndex(parent, val uint32) int { + t.muVisibleItems.RLock() + defer t.muVisibleItems.RUnlock() + for i, itemval := range t.visibleItems[parent] { + if val == itemval { + return i + } + } + return -1 +} + +// Loads an image from file to be shown in tray or menu item. +// LoadImage: https://msdn.microsoft.com/en-us/library/windows/desktop/ms648045(v=vs.85).aspx +func (t *winTray) loadIconFrom(src string) (windows.Handle, error) { + const IMAGE_ICON = 1 // Loads an icon + const LR_LOADFROMFILE = 0x00000010 // Loads the stand-alone image from the file + const LR_DEFAULTSIZE = 0x00000040 // Loads default-size icon for windows(SM_CXICON x SM_CYICON) if cx, cy are set to zero + + // Save and reuse handles of loaded images + t.muLoadedImages.RLock() + h, ok := t.loadedImages[src] + t.muLoadedImages.RUnlock() + if !ok { + srcPtr, err := windows.UTF16PtrFromString(src) + if err != nil { + return 0, err + } + res, _, err := pLoadImage.Call( + 0, + uintptr(unsafe.Pointer(srcPtr)), + IMAGE_ICON, + 0, + 0, + LR_LOADFROMFILE|LR_DEFAULTSIZE, + ) + if res == 0 { + return 0, err + } + h = windows.Handle(res) + t.muLoadedImages.Lock() + t.loadedImages[src] = h + t.muLoadedImages.Unlock() + } + return h, nil +} + +func (t *winTray) iconToBitmap(hIcon windows.Handle) (windows.Handle, error) { + const SM_CXSMICON = 49 + const SM_CYSMICON = 50 + const DI_NORMAL = 0x3 + hDC, _, err := pGetDC.Call(uintptr(0)) + if hDC == 0 { + return 0, err + } + defer pReleaseDC.Call(uintptr(0), hDC) + hMemDC, _, err := pCreateCompatibleDC.Call(hDC) + if hMemDC == 0 { + return 0, err + } + defer pDeleteDC.Call(hMemDC) + cx, _, _ := pGetSystemMetrics.Call(SM_CXSMICON) + cy, _, _ := pGetSystemMetrics.Call(SM_CYSMICON) + hMemBmp, _, err := pCreateCompatibleBitmap.Call(hDC, cx, cy) + if hMemBmp == 0 { + return 0, err + } + hOriginalBmp, _, _ := pSelectObject.Call(hMemDC, hMemBmp) + defer pSelectObject.Call(hMemDC, hOriginalBmp) + res, _, err := pDrawIconEx.Call(hMemDC, 0, 0, uintptr(hIcon), cx, cy, 0, uintptr(0), DI_NORMAL) + if res == 0 { + return 0, err + } + return windows.Handle(hMemBmp), nil +} + +func registerSystray() { + if err := wt.initInstance(); err != nil { + log.Errorf("Unable to init instance: %v", err) + return + } + + if err := wt.createMenu(); err != nil { + log.Errorf("Unable to create menu: %v", err) + return + } + +} + +func nativeLoop() { + // 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, + ) +} + +func iconBytesToFilePath(iconBytes []byte) (string, error) { + bh := md5.Sum(iconBytes) + dataHash := hex.EncodeToString(bh[:]) + iconFilePath := filepath.Join(os.TempDir(), "systray_temp_icon_"+dataHash) + + if _, err := os.Stat(iconFilePath); os.IsNotExist(err) { + if err := ioutil.WriteFile(iconFilePath, iconBytes, 0644); err != nil { + return "", err + } + } + return iconFilePath, nil +} + +// SetIcon sets the systray icon. +// iconBytes should be the content of .ico for windows and .ico/.jpg/.png +// for other platforms. +func SetIcon(iconBytes []byte) { + iconFilePath, err := iconBytesToFilePath(iconBytes) + if err != nil { + log.Errorf("Unable to write icon data to temp file: %v", err) + return + } + if err := wt.setIcon(iconFilePath); err != nil { + log.Errorf("Unable to set icon: %v", err) + return + } +} + +// SetTemplateIcon sets the systray icon as a template icon (on macOS), falling back +// to a regular icon on other platforms. +// templateIconBytes and iconBytes should be the content of .ico for windows and +// .ico/.jpg/.png for other platforms. +func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { + SetIcon(regularIconBytes) +} + +// SetTitle sets the systray title, only available on Mac and Linux. +func SetTitle(title string) { + // do nothing +} + +func (item *MenuItem) parentId() uint32 { + if item.parent != nil { + return uint32(item.parent.id) + } + return 0 +} + +// SetIcon sets the icon of a menu item. Only works on macOS and Windows. +// iconBytes should be the content of .ico/.jpg/.png +func (item *MenuItem) SetIcon(iconBytes []byte) { + iconFilePath, err := iconBytesToFilePath(iconBytes) + if err != nil { + log.Errorf("Unable to write icon data to temp file: %v", err) + return + } + + h, err := wt.loadIconFrom(iconFilePath) + if err != nil { + log.Errorf("Unable to load icon from temp file: %v", err) + return + } + + h, err = wt.iconToBitmap(h) + if err != nil { + log.Errorf("Unable to convert icon to bitmap: %v", err) + return + } + wt.muMenuItemIcons.Lock() + wt.menuItemIcons[uint32(item.id)] = h + wt.muMenuItemIcons.Unlock() + + err = wt.addOrUpdateMenuItem(uint32(item.id), item.parentId(), item.title, item.disabled, item.checked) + if err != nil { + log.Errorf("Unable to addOrUpdateMenuItem: %v", err) + return + } +} + +// SetTooltip sets the systray tooltip to display on mouse hover of the tray icon, +// 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(uint32(item.id), item.parentId(), item.title, item.disabled, item.checked) + if err != nil { + log.Errorf("Unable to addOrUpdateMenuItem: %v", err) + return + } +} + +// SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows, it +// falls back to the regular icon bytes and on Linux it does nothing. +// templateIconBytes and regularIconBytes should be the content of .ico for windows and +// .ico/.jpg/.png for other platforms. +func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { + item.SetIcon(regularIconBytes) +} + +func addSeparator(id uint32) { + err := wt.addSeparatorMenuItem(id, 0) + if err != nil { + log.Errorf("Unable to addSeparator: %v", err) + return + } +} + +func hideMenuItem(item *MenuItem) { + err := wt.hideMenuItem(uint32(item.id), item.parentId()) + if err != nil { + log.Errorf("Unable to hideMenuItem: %v", err) + return + } +} + +func showMenuItem(item *MenuItem) { + addOrUpdateMenuItem(item) +} diff --git a/src/systray/systray_windows_test.go b/src/systray/systray_windows_test.go new file mode 100644 index 0000000..7cb6c75 --- /dev/null +++ b/src/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) +}