systray_unix.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. //go:build linux || freebsd || openbsd || netbsd
  2. // +build linux freebsd openbsd netbsd
  3. //Note that you need to have github.com/knightpp/dbus-codegen-go installed from "custom" branch
  4. //go:generate dbus-codegen-go -prefix org.kde -package notifier -output internal/generated/notifier/status_notifier_item.go internal/StatusNotifierItem.xml
  5. //go:generate dbus-codegen-go -prefix com.canonical -package menu -output internal/generated/menu/dbus_menu.go internal/DbusMenu.xml
  6. package systray
  7. import (
  8. "bytes"
  9. "fmt"
  10. "image"
  11. _ "image/png" // used only here
  12. "log"
  13. "os"
  14. "sync"
  15. "github.com/godbus/dbus/v5"
  16. "github.com/godbus/dbus/v5/introspect"
  17. "github.com/godbus/dbus/v5/prop"
  18. "fyne.io/systray/internal/generated/menu"
  19. "fyne.io/systray/internal/generated/notifier"
  20. )
  21. const (
  22. path = "/StatusNotifierItem"
  23. menuPath = "/StatusNotifierMenu"
  24. )
  25. var (
  26. // to signal quitting the internal main loop
  27. quitChan = make(chan struct{})
  28. // instance is the current instance of our DBus tray server
  29. instance = &tray{menu: &menuLayout{}, menuVersion: 1}
  30. )
  31. // SetTemplateIcon sets the systray icon as a template icon (on macOS), falling back
  32. // to a regular icon on other platforms.
  33. // templateIconBytes and iconBytes should be the content of .ico for windows and
  34. // .ico/.jpg/.png for other platforms.
  35. func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) {
  36. // TODO handle the templateIconBytes?
  37. SetIcon(regularIconBytes)
  38. }
  39. // SetIcon sets the systray icon.
  40. // iconBytes should be the content of .ico for windows and .ico/.jpg/.png
  41. // for other platforms.
  42. func SetIcon(iconBytes []byte) {
  43. instance.lock.Lock()
  44. instance.iconData = iconBytes
  45. props := instance.props
  46. conn := instance.conn
  47. defer instance.lock.Unlock()
  48. if props == nil {
  49. return
  50. }
  51. props.SetMust("org.kde.StatusNotifierItem", "IconPixmap",
  52. []PX{convertToPixels(iconBytes)})
  53. if conn == nil {
  54. return
  55. }
  56. err := notifier.Emit(conn, &notifier.StatusNotifierItem_NewIconSignal{
  57. Path: path,
  58. Body: &notifier.StatusNotifierItem_NewIconSignalBody{},
  59. })
  60. if err != nil {
  61. log.Printf("systray error: failed to emit new icon signal: %s\n", err)
  62. return
  63. }
  64. }
  65. // SetTitle sets the systray title, only available on Mac and Linux.
  66. func SetTitle(t string) {
  67. instance.lock.Lock()
  68. instance.title = t
  69. props := instance.props
  70. conn := instance.conn
  71. defer instance.lock.Unlock()
  72. if props == nil {
  73. return
  74. }
  75. dbusErr := props.Set("org.kde.StatusNotifierItem", "Title",
  76. dbus.MakeVariant(t))
  77. if dbusErr != nil {
  78. log.Printf("systray error: failed to set Title prop: %s\n", dbusErr)
  79. return
  80. }
  81. if conn == nil {
  82. return
  83. }
  84. err := notifier.Emit(conn, &notifier.StatusNotifierItem_NewTitleSignal{
  85. Path: path,
  86. Body: &notifier.StatusNotifierItem_NewTitleSignalBody{},
  87. })
  88. if err != nil {
  89. log.Printf("systray error: failed to emit new title signal: %s\n", err)
  90. return
  91. }
  92. }
  93. // SetTooltip sets the systray tooltip to display on mouse hover of the tray icon,
  94. // only available on Mac and Windows.
  95. func SetTooltip(tooltipTitle string) {
  96. instance.lock.Lock()
  97. instance.tooltipTitle = tooltipTitle
  98. props := instance.props
  99. defer instance.lock.Unlock()
  100. if props == nil {
  101. return
  102. }
  103. dbusErr := props.Set("org.kde.StatusNotifierItem", "ToolTip",
  104. dbus.MakeVariant(tooltip{V2: tooltipTitle}))
  105. if dbusErr != nil {
  106. log.Printf("systray error: failed to set ToolTip prop: %s\n", dbusErr)
  107. return
  108. }
  109. }
  110. // SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows and
  111. // Linux, it falls back to the regular icon bytes.
  112. // templateIconBytes and regularIconBytes should be the content of .ico for windows and
  113. // .ico/.jpg/.png for other platforms.
  114. func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) {
  115. item.SetIcon(regularIconBytes)
  116. }
  117. func setInternalLoop(_ bool) {
  118. // nothing to action on Linux
  119. }
  120. func registerSystray() {
  121. }
  122. func nativeLoop() int {
  123. nativeStart()
  124. <-quitChan
  125. nativeEnd()
  126. return 0
  127. }
  128. func nativeEnd() {
  129. runSystrayExit()
  130. instance.conn.Close()
  131. }
  132. func quit() {
  133. close(quitChan)
  134. }
  135. func nativeStart() {
  136. systrayReady()
  137. conn, err := dbus.SessionBus()
  138. if err != nil {
  139. log.Printf("systray error: failed to connect to DBus: %v\n", err)
  140. return
  141. }
  142. err = notifier.ExportStatusNotifierItem(conn, path, &notifier.UnimplementedStatusNotifierItem{})
  143. if err != nil {
  144. log.Printf("systray error: failed to export status notifier item: %v\n", err)
  145. }
  146. err = menu.ExportDbusmenu(conn, menuPath, instance)
  147. if err != nil {
  148. log.Printf("systray error: failed to export status notifier menu: %v\n", err)
  149. return
  150. }
  151. name := fmt.Sprintf("org.kde.StatusNotifierItem-%d-1", os.Getpid()) // register id 1 for this process
  152. _, err = conn.RequestName(name, dbus.NameFlagDoNotQueue)
  153. if err != nil {
  154. log.Printf("systray error: failed to request name: %s\n", err)
  155. // it's not critical error: continue
  156. }
  157. props, err := prop.Export(conn, path, instance.createPropSpec())
  158. if err != nil {
  159. log.Printf("systray error: failed to export notifier item properties to bus: %s\n", err)
  160. return
  161. }
  162. menuProps, err := prop.Export(conn, menuPath, createMenuPropSpec())
  163. if err != nil {
  164. log.Printf("systray error: failed to export notifier menu properties to bus: %s\n", err)
  165. return
  166. }
  167. node := introspect.Node{
  168. Name: path,
  169. Interfaces: []introspect.Interface{
  170. introspect.IntrospectData,
  171. prop.IntrospectData,
  172. notifier.IntrospectDataStatusNotifierItem,
  173. },
  174. }
  175. err = conn.Export(introspect.NewIntrospectable(&node), path,
  176. "org.freedesktop.DBus.Introspectable")
  177. if err != nil {
  178. log.Printf("systray error: failed to export node introspection: %s\n", err)
  179. return
  180. }
  181. menuNode := introspect.Node{
  182. Name: menuPath,
  183. Interfaces: []introspect.Interface{
  184. introspect.IntrospectData,
  185. prop.IntrospectData,
  186. menu.IntrospectDataDbusmenu,
  187. },
  188. }
  189. err = conn.Export(introspect.NewIntrospectable(&menuNode), menuPath,
  190. "org.freedesktop.DBus.Introspectable")
  191. if err != nil {
  192. log.Printf("systray error: failed to export menu node introspection: %s\n", err)
  193. return
  194. }
  195. instance.lock.Lock()
  196. instance.conn = conn
  197. instance.props = props
  198. instance.menuProps = menuProps
  199. instance.lock.Unlock()
  200. go stayRegistered()
  201. }
  202. func register() bool {
  203. obj := instance.conn.Object("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher")
  204. call := obj.Call("org.kde.StatusNotifierWatcher.RegisterStatusNotifierItem", 0, path)
  205. if call.Err != nil {
  206. log.Printf("systray error: failed to register: %v\n", call.Err)
  207. return false
  208. }
  209. return true
  210. }
  211. func stayRegistered() {
  212. register()
  213. conn := instance.conn
  214. if err := conn.AddMatchSignal(
  215. dbus.WithMatchObjectPath("/org/freedesktop/DBus"),
  216. dbus.WithMatchInterface("org.freedesktop.DBus"),
  217. dbus.WithMatchSender("org.freedesktop.DBus"),
  218. dbus.WithMatchMember("NameOwnerChanged"),
  219. dbus.WithMatchArg(0, "org.kde.StatusNotifierWatcher"),
  220. ); err != nil {
  221. log.Printf("systray error: failed to register signal matching: %v\n", err)
  222. // If we can't monitor signals, there is no point in
  223. // us being here. we're either registered or not (per
  224. // above) and will roll the dice from here...
  225. return
  226. }
  227. sc := make(chan *dbus.Signal, 10)
  228. conn.Signal(sc)
  229. for {
  230. select {
  231. case sig := <-sc:
  232. if sig == nil {
  233. return // We get a nil signal when closing the window.
  234. } else if len(sig.Body) < 3 {
  235. return // malformed signal?
  236. }
  237. // sig.Body has the args, which are [name old_owner new_owner]
  238. if s, ok := sig.Body[2].(string); ok && s != "" {
  239. register()
  240. }
  241. case <-quitChan:
  242. return
  243. }
  244. }
  245. }
  246. // tray is a basic type that handles the dbus functionality
  247. type tray struct {
  248. // the DBus connection that we will use
  249. conn *dbus.Conn
  250. // icon data for the main systray icon
  251. iconData []byte
  252. // title and tooltip state
  253. title, tooltipTitle string
  254. lock sync.Mutex
  255. menu *menuLayout
  256. menuLock sync.RWMutex
  257. props, menuProps *prop.Properties
  258. menuVersion uint32
  259. }
  260. func (t *tray) createPropSpec() map[string]map[string]*prop.Prop {
  261. t.lock.Lock()
  262. defer t.lock.Unlock()
  263. id := t.title
  264. if id == "" {
  265. id = fmt.Sprintf("systray_%d", os.Getpid())
  266. }
  267. return map[string]map[string]*prop.Prop{
  268. "org.kde.StatusNotifierItem": {
  269. "Status": {
  270. Value: "Active", // Passive, Active or NeedsAttention
  271. Writable: false,
  272. Emit: prop.EmitTrue,
  273. Callback: nil,
  274. },
  275. "Title": {
  276. Value: t.title,
  277. Writable: true,
  278. Emit: prop.EmitTrue,
  279. Callback: nil,
  280. },
  281. "Id": {
  282. Value: id,
  283. Writable: false,
  284. Emit: prop.EmitTrue,
  285. Callback: nil,
  286. },
  287. "Category": {
  288. Value: "ApplicationStatus",
  289. Writable: false,
  290. Emit: prop.EmitTrue,
  291. Callback: nil,
  292. },
  293. "IconName": {
  294. Value: "",
  295. Writable: false,
  296. Emit: prop.EmitTrue,
  297. Callback: nil,
  298. },
  299. "IconPixmap": {
  300. Value: []PX{convertToPixels(t.iconData)},
  301. Writable: true,
  302. Emit: prop.EmitTrue,
  303. Callback: nil,
  304. },
  305. "IconThemePath": {
  306. Value: "",
  307. Writable: false,
  308. Emit: prop.EmitTrue,
  309. Callback: nil,
  310. },
  311. "ItemIsMenu": {
  312. Value: true,
  313. Writable: false,
  314. Emit: prop.EmitTrue,
  315. Callback: nil,
  316. },
  317. "Menu": {
  318. Value: dbus.ObjectPath(menuPath),
  319. Writable: true,
  320. Emit: prop.EmitTrue,
  321. Callback: nil,
  322. },
  323. "ToolTip": {
  324. Value: tooltip{V2: t.tooltipTitle},
  325. Writable: true,
  326. Emit: prop.EmitTrue,
  327. Callback: nil,
  328. },
  329. }}
  330. }
  331. // PX is picture pix map structure with width and high
  332. type PX struct {
  333. W, H int
  334. Pix []byte
  335. }
  336. // tooltip is our data for a tooltip property.
  337. // Param names need to match the generated code...
  338. type tooltip = struct {
  339. V0 string // name
  340. V1 []PX // icons
  341. V2 string // title
  342. V3 string // description
  343. }
  344. func convertToPixels(data []byte) PX {
  345. if len(data) == 0 {
  346. return PX{}
  347. }
  348. img, _, err := image.Decode(bytes.NewReader(data))
  349. if err != nil {
  350. log.Printf("Failed to read icon format %v", err)
  351. return PX{}
  352. }
  353. return PX{
  354. img.Bounds().Dx(), img.Bounds().Dy(),
  355. argbForImage(img),
  356. }
  357. }
  358. func argbForImage(img image.Image) []byte {
  359. w, h := img.Bounds().Dx(), img.Bounds().Dy()
  360. data := make([]byte, w*h*4)
  361. i := 0
  362. for y := 0; y < h; y++ {
  363. for x := 0; x < w; x++ {
  364. r, g, b, a := img.At(x, y).RGBA()
  365. data[i] = byte(a)
  366. data[i+1] = byte(r)
  367. data[i+2] = byte(g)
  368. data[i+3] = byte(b)
  369. i += 4
  370. }
  371. }
  372. return data
  373. }