systray_unix.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  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. dbusErr := props.Set("org.kde.StatusNotifierItem", "IconPixmap",
  52. dbus.MakeVariant([]PX{convertToPixels(iconBytes)}))
  53. if dbusErr != nil {
  54. log.Printf("systray error: failed to set IconPixmap prop: %s\n", dbusErr)
  55. return
  56. }
  57. if conn == nil {
  58. return
  59. }
  60. err := notifier.Emit(conn, &notifier.StatusNotifierItem_NewIconSignal{
  61. Path: path,
  62. Body: &notifier.StatusNotifierItem_NewIconSignalBody{},
  63. })
  64. if err != nil {
  65. log.Printf("systray error: failed to emit new icon signal: %s\n", err)
  66. return
  67. }
  68. }
  69. // SetTitle sets the systray title, only available on Mac and Linux.
  70. func SetTitle(t string) {
  71. instance.lock.Lock()
  72. instance.title = t
  73. props := instance.props
  74. conn := instance.conn
  75. defer instance.lock.Unlock()
  76. if props == nil {
  77. return
  78. }
  79. dbusErr := props.Set("org.kde.StatusNotifierItem", "Title",
  80. dbus.MakeVariant(t))
  81. if dbusErr != nil {
  82. log.Printf("systray error: failed to set Title prop: %s\n", dbusErr)
  83. return
  84. }
  85. if conn == nil {
  86. return
  87. }
  88. err := notifier.Emit(conn, &notifier.StatusNotifierItem_NewTitleSignal{
  89. Path: path,
  90. Body: &notifier.StatusNotifierItem_NewTitleSignalBody{},
  91. })
  92. if err != nil {
  93. log.Printf("systray error: failed to emit new title signal: %s\n", err)
  94. return
  95. }
  96. }
  97. // SetTooltip sets the systray tooltip to display on mouse hover of the tray icon,
  98. // only available on Mac and Windows.
  99. func SetTooltip(tooltipTitle string) {
  100. instance.lock.Lock()
  101. instance.tooltipTitle = tooltipTitle
  102. props := instance.props
  103. defer instance.lock.Unlock()
  104. if props == nil {
  105. return
  106. }
  107. dbusErr := props.Set("org.kde.StatusNotifierItem", "ToolTip",
  108. dbus.MakeVariant(tooltip{V2: tooltipTitle}))
  109. if dbusErr != nil {
  110. log.Printf("systray error: failed to set ToolTip prop: %s\n", dbusErr)
  111. return
  112. }
  113. }
  114. // SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows and
  115. // Linux, it falls back to the regular icon bytes.
  116. // templateIconBytes and regularIconBytes should be the content of .ico for windows and
  117. // .ico/.jpg/.png for other platforms.
  118. func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) {
  119. item.SetIcon(regularIconBytes)
  120. }
  121. func setInternalLoop(_ bool) {
  122. // nothing to action on Linux
  123. }
  124. func registerSystray() {
  125. }
  126. func nativeLoop() int {
  127. nativeStart()
  128. <-quitChan
  129. nativeEnd()
  130. return 0
  131. }
  132. func nativeEnd() {
  133. runSystrayExit()
  134. instance.conn.Close()
  135. }
  136. func quit() {
  137. close(quitChan)
  138. }
  139. func nativeStart() {
  140. systrayReady()
  141. conn, _ := dbus.ConnectSessionBus()
  142. if conn == nil {
  143. log.Printf("systray error: failed to connect to DBus")
  144. return
  145. }
  146. err := notifier.ExportStatusNotifierItem(conn, path, &notifier.UnimplementedStatusNotifierItem{})
  147. if err != nil {
  148. log.Printf("systray error: failed to export status notifier item: %s\n", err)
  149. }
  150. err = menu.ExportDbusmenu(conn, menuPath, instance)
  151. if err != nil {
  152. log.Printf("systray error: failed to export status notifier item: %s\n", err)
  153. return
  154. }
  155. name := fmt.Sprintf("org.kde.StatusNotifierItem-%d-1", os.Getpid()) // register id 1 for this process
  156. _, err = conn.RequestName(name, dbus.NameFlagDoNotQueue)
  157. if err != nil {
  158. log.Printf("systray error: failed to request name: %s\n", err)
  159. // it's not critical error: continue
  160. }
  161. props, err := prop.Export(conn, path, instance.createPropSpec())
  162. if err != nil {
  163. log.Printf("systray error: failed to export notifier item properties to bus: %s\n", err)
  164. return
  165. }
  166. menuProps, err := prop.Export(conn, menuPath, createMenuPropSpec())
  167. if err != nil {
  168. log.Printf("systray error: failed to export notifier menu properties to bus: %s\n", err)
  169. return
  170. }
  171. node := introspect.Node{
  172. Name: path,
  173. Interfaces: []introspect.Interface{
  174. introspect.IntrospectData,
  175. prop.IntrospectData,
  176. notifier.IntrospectDataStatusNotifierItem,
  177. },
  178. }
  179. err = conn.Export(introspect.NewIntrospectable(&node), path,
  180. "org.freedesktop.DBus.Introspectable")
  181. if err != nil {
  182. log.Printf("systray error: failed to export node introspection: %s\n", err)
  183. return
  184. }
  185. menuNode := introspect.Node{
  186. Name: menuPath,
  187. Interfaces: []introspect.Interface{
  188. introspect.IntrospectData,
  189. prop.IntrospectData,
  190. menu.IntrospectDataDbusmenu,
  191. },
  192. }
  193. err = conn.Export(introspect.NewIntrospectable(&menuNode), menuPath,
  194. "org.freedesktop.DBus.Introspectable")
  195. if err != nil {
  196. log.Printf("systray error: failed to export menu node introspection: %s\n", err)
  197. return
  198. }
  199. instance.lock.Lock()
  200. instance.conn = conn
  201. instance.props = props
  202. instance.menuProps = menuProps
  203. instance.lock.Unlock()
  204. obj := conn.Object("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher")
  205. call := obj.Call("org.kde.StatusNotifierWatcher.RegisterStatusNotifierItem", 0, path)
  206. if call.Err != nil {
  207. log.Printf("systray error: failed to register our icon with the notifier watcher (maybe no tray is running?): %s\n", call.Err)
  208. }
  209. }
  210. // tray is a basic type that handles the dbus functionality
  211. type tray struct {
  212. // the DBus connection that we will use
  213. conn *dbus.Conn
  214. // icon data for the main systray icon
  215. iconData []byte
  216. // title and tooltip state
  217. title, tooltipTitle string
  218. lock sync.Mutex
  219. menu *menuLayout
  220. menuLock sync.RWMutex
  221. props, menuProps *prop.Properties
  222. menuVersion uint32
  223. }
  224. func (t *tray) createPropSpec() map[string]map[string]*prop.Prop {
  225. t.lock.Lock()
  226. t.lock.Unlock()
  227. return map[string]map[string]*prop.Prop{
  228. "org.kde.StatusNotifierItem": {
  229. "Status": {
  230. Value: "Active", // Passive, Active or NeedsAttention
  231. Writable: false,
  232. Emit: prop.EmitTrue,
  233. Callback: nil,
  234. },
  235. "Title": {
  236. Value: t.title,
  237. Writable: true,
  238. Emit: prop.EmitTrue,
  239. Callback: nil,
  240. },
  241. "Id": {
  242. Value: "1",
  243. Writable: false,
  244. Emit: prop.EmitTrue,
  245. Callback: nil,
  246. },
  247. "Category": {
  248. Value: "ApplicationStatus",
  249. Writable: false,
  250. Emit: prop.EmitTrue,
  251. Callback: nil,
  252. },
  253. "IconName": {
  254. Value: "",
  255. Writable: false,
  256. Emit: prop.EmitTrue,
  257. Callback: nil,
  258. },
  259. "IconPixmap": {
  260. Value: []PX{convertToPixels(t.iconData)},
  261. Writable: true,
  262. Emit: prop.EmitTrue,
  263. Callback: nil,
  264. },
  265. "IconThemePath": {
  266. Value: "",
  267. Writable: false,
  268. Emit: prop.EmitTrue,
  269. Callback: nil,
  270. },
  271. "ItemIsMenu": {
  272. Value: true,
  273. Writable: false,
  274. Emit: prop.EmitTrue,
  275. Callback: nil,
  276. },
  277. "Menu": {
  278. Value: dbus.ObjectPath(menuPath),
  279. Writable: true,
  280. Emit: prop.EmitTrue,
  281. Callback: nil,
  282. },
  283. "ToolTip": {
  284. Value: tooltip{V2: t.tooltipTitle},
  285. Writable: true,
  286. Emit: prop.EmitTrue,
  287. Callback: nil,
  288. },
  289. }}
  290. }
  291. // PX is picture pix map structure with width and high
  292. type PX struct {
  293. W, H int
  294. Pix []byte
  295. }
  296. // tooltip is our data for a tooltip property.
  297. // Param names need to match the generated code...
  298. type tooltip = struct {
  299. V0 string // name
  300. V1 []PX // icons
  301. V2 string // title
  302. V3 string // description
  303. }
  304. func convertToPixels(data []byte) PX {
  305. if len(data) == 0 {
  306. return PX{}
  307. }
  308. img, _, err := image.Decode(bytes.NewReader(data))
  309. if err != nil {
  310. log.Printf("Failed to read icon format %v", err)
  311. return PX{}
  312. }
  313. return PX{
  314. img.Bounds().Dx(), img.Bounds().Dy(),
  315. argbForImage(img),
  316. }
  317. }
  318. func argbForImage(img image.Image) []byte {
  319. w, h := img.Bounds().Dx(), img.Bounds().Dy()
  320. data := make([]byte, w*h*4)
  321. i := 0
  322. for y := 0; y < h; y++ {
  323. for x := 0; x < w; x++ {
  324. r, g, b, a := img.At(x, y).RGBA()
  325. data[i] = byte(a)
  326. data[i+1] = byte(r)
  327. data[i+2] = byte(g)
  328. data[i+3] = byte(b)
  329. i += 4
  330. }
  331. }
  332. return data
  333. }