mirror of
https://github.com/nkanaev/yarr.git
synced 2025-05-24 00:33:14 +00:00
include systray
This commit is contained in:
parent
514ed02693
commit
e79abb69eb
2
go.mod
2
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
|
||||
|
22
go.sum
22
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=
|
||||
|
@ -3,7 +3,7 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"github.com/getlantern/systray"
|
||||
"github.com/nkanaev/yarr/src/systray"
|
||||
"github.com/nkanaev/yarr/src/server"
|
||||
)
|
||||
|
||||
|
12
src/systray/.gitignore
vendored
Normal file
12
src/systray/.gitignore
vendored
Normal file
@ -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
|
125
src/systray/CHANGELOG.md
Normal file
125
src/systray/CHANGELOG.md
Normal file
@ -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)*
|
202
src/systray/LICENSE
Normal file
202
src/systray/LICENSE
Normal file
@ -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.
|
18
src/systray/Makefile
Normal file
18
src/systray/Makefile
Normal file
@ -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
|
11
src/systray/NOTES.txt
Normal file
11
src/systray/NOTES.txt
Normal file
@ -0,0 +1,11 @@
|
||||
taken from:
|
||||
|
||||
repo:
|
||||
https://github.com/getlantern/systray
|
||||
|
||||
hash:
|
||||
2c0986dda9aea361e925f90e848d9036be7b5367
|
||||
|
||||
changes:
|
||||
|
||||
-removed `getlantern/golog` dependency
|
120
src/systray/README.md
Normal file
120
src/systray/README.md
Normal file
@ -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!
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
<!-- avoid having a blurry icon and text -->
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>True</string>
|
||||
|
||||
<!-- avoid showing the app on the Dock -->
|
||||
<key>LSUIElement</key>
|
||||
<string>1</string>
|
||||
```
|
||||
|
||||
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
|
233
src/systray/systray.go
Normal file
233
src/systray/systray.go
Normal file
@ -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:
|
||||
}
|
||||
}
|
17
src/systray/systray.h
Normal file
17
src/systray/systray.h
Normal file
@ -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();
|
38
src/systray/systray_darwin.go
Normal file
38
src/systray/systray_darwin.go
Normal file
@ -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)
|
||||
}
|
293
src/systray/systray_darwin.m
Normal file
293
src/systray/systray_darwin.m
Normal file
@ -0,0 +1,293 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#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 <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 {
|
||||
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);
|
||||
}
|
268
src/systray/systray_linux.c
Normal file
268
src/systray/systray_linux.c
Normal file
@ -0,0 +1,268 @@
|
||||
#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;
|
||||
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);
|
||||
}
|
29
src/systray/systray_linux.go
Normal file
29
src/systray/systray_linux.go
Normal file
@ -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) {
|
||||
}
|
106
src/systray/systray_nonwindows.go
Normal file
106
src/systray/systray_nonwindows.go
Normal file
@ -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))
|
||||
}
|
939
src/systray/systray_windows.go
Normal file
939
src/systray/systray_windows.go
Normal file
@ -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)
|
||||
}
|
132
src/systray/systray_windows_test.go
Normal file
132
src/systray/systray_windows_test.go
Normal file
@ -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)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user