systray_unix.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  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. obj := conn.Object("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher")
  201. call := obj.Call("org.kde.StatusNotifierWatcher.RegisterStatusNotifierItem", 0, path)
  202. if call.Err != nil {
  203. log.Printf("systray error: failed to register our icon with the notifier watcher (maybe no tray is running?): %s\n", call.Err)
  204. }
  205. }
  206. // tray is a basic type that handles the dbus functionality
  207. type tray struct {
  208. // the DBus connection that we will use
  209. conn *dbus.Conn
  210. // icon data for the main systray icon
  211. iconData []byte
  212. // title and tooltip state
  213. title, tooltipTitle string
  214. lock sync.Mutex
  215. menu *menuLayout
  216. menuLock sync.RWMutex
  217. props, menuProps *prop.Properties
  218. menuVersion uint32
  219. }
  220. func (t *tray) createPropSpec() map[string]map[string]*prop.Prop {
  221. t.lock.Lock()
  222. t.lock.Unlock()
  223. return map[string]map[string]*prop.Prop{
  224. "org.kde.StatusNotifierItem": {
  225. "Status": {
  226. Value: "Active", // Passive, Active or NeedsAttention
  227. Writable: false,
  228. Emit: prop.EmitTrue,
  229. Callback: nil,
  230. },
  231. "Title": {
  232. Value: t.title,
  233. Writable: true,
  234. Emit: prop.EmitTrue,
  235. Callback: nil,
  236. },
  237. "Id": {
  238. Value: "1",
  239. Writable: false,
  240. Emit: prop.EmitTrue,
  241. Callback: nil,
  242. },
  243. "Category": {
  244. Value: "ApplicationStatus",
  245. Writable: false,
  246. Emit: prop.EmitTrue,
  247. Callback: nil,
  248. },
  249. "IconName": {
  250. Value: "",
  251. Writable: false,
  252. Emit: prop.EmitTrue,
  253. Callback: nil,
  254. },
  255. "IconPixmap": {
  256. Value: []PX{convertToPixels(t.iconData)},
  257. Writable: true,
  258. Emit: prop.EmitTrue,
  259. Callback: nil,
  260. },
  261. "IconThemePath": {
  262. Value: "",
  263. Writable: false,
  264. Emit: prop.EmitTrue,
  265. Callback: nil,
  266. },
  267. "ItemIsMenu": {
  268. Value: true,
  269. Writable: false,
  270. Emit: prop.EmitTrue,
  271. Callback: nil,
  272. },
  273. "Menu": {
  274. Value: dbus.ObjectPath(menuPath),
  275. Writable: true,
  276. Emit: prop.EmitTrue,
  277. Callback: nil,
  278. },
  279. "ToolTip": {
  280. Value: tooltip{V2: t.tooltipTitle},
  281. Writable: true,
  282. Emit: prop.EmitTrue,
  283. Callback: nil,
  284. },
  285. }}
  286. }
  287. // PX is picture pix map structure with width and high
  288. type PX struct {
  289. W, H int
  290. Pix []byte
  291. }
  292. // tooltip is our data for a tooltip property.
  293. // Param names need to match the generated code...
  294. type tooltip = struct {
  295. V0 string // name
  296. V1 []PX // icons
  297. V2 string // title
  298. V3 string // description
  299. }
  300. func convertToPixels(data []byte) PX {
  301. if len(data) == 0 {
  302. return PX{}
  303. }
  304. img, _, err := image.Decode(bytes.NewReader(data))
  305. if err != nil {
  306. log.Printf("Failed to read icon format %v", err)
  307. return PX{}
  308. }
  309. return PX{
  310. img.Bounds().Dx(), img.Bounds().Dy(),
  311. argbForImage(img),
  312. }
  313. }
  314. func argbForImage(img image.Image) []byte {
  315. w, h := img.Bounds().Dx(), img.Bounds().Dy()
  316. data := make([]byte, w*h*4)
  317. i := 0
  318. for y := 0; y < h; y++ {
  319. for x := 0; x < w; x++ {
  320. r, g, b, a := img.At(x, y).RGBA()
  321. data[i] = byte(a)
  322. data[i+1] = byte(r)
  323. data[i+2] = byte(g)
  324. data[i+3] = byte(b)
  325. i += 4
  326. }
  327. }
  328. return data
  329. }