systray_menu_unix.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. //go:build linux || freebsd || openbsd || netbsd
  2. // +build linux freebsd openbsd netbsd
  3. package systray
  4. import (
  5. "log"
  6. "github.com/godbus/dbus/v5"
  7. "github.com/godbus/dbus/v5/prop"
  8. "fyne.io/systray/internal/generated/menu"
  9. )
  10. // SetIcon sets the icon of a menu item.
  11. // iconBytes should be the content of .ico/.jpg/.png
  12. func (item *MenuItem) SetIcon(iconBytes []byte) {
  13. instance.menuLock.Lock()
  14. defer instance.menuLock.Unlock()
  15. m, exists := findLayout(int32(item.id))
  16. if exists {
  17. m.V1["icon-data"] = dbus.MakeVariant(iconBytes)
  18. refresh()
  19. }
  20. }
  21. // copyLayout makes full copy of layout
  22. func copyLayout(in *menuLayout, depth int32) *menuLayout {
  23. out := menuLayout{
  24. V0: in.V0,
  25. V1: make(map[string]dbus.Variant, len(in.V1)),
  26. }
  27. for k, v := range in.V1 {
  28. out.V1[k] = v
  29. }
  30. if depth != 0 {
  31. depth--
  32. out.V2 = make([]dbus.Variant, len(in.V2))
  33. for i, v := range in.V2 {
  34. out.V2[i] = dbus.MakeVariant(copyLayout(v.Value().(*menuLayout), depth))
  35. }
  36. } else {
  37. out.V2 = []dbus.Variant{}
  38. }
  39. return &out
  40. }
  41. // GetLayout is com.canonical.dbusmenu.GetLayout method.
  42. func (t *tray) GetLayout(parentID int32, recursionDepth int32, propertyNames []string) (revision uint32, layout menuLayout, err *dbus.Error) {
  43. instance.menuLock.Lock()
  44. defer instance.menuLock.Unlock()
  45. if m, ok := findLayout(parentID); ok {
  46. // return copy of menu layout to prevent panic from cuncurrent access to layout
  47. return instance.menuVersion, *copyLayout(m, recursionDepth), nil
  48. }
  49. return
  50. }
  51. // GetGroupProperties is com.canonical.dbusmenu.GetGroupProperties method.
  52. func (t *tray) GetGroupProperties(ids []int32, propertyNames []string) (properties []struct {
  53. V0 int32
  54. V1 map[string]dbus.Variant
  55. }, err *dbus.Error) {
  56. instance.menuLock.Lock()
  57. defer instance.menuLock.Unlock()
  58. for _, id := range ids {
  59. if m, ok := findLayout(id); ok {
  60. p := struct {
  61. V0 int32
  62. V1 map[string]dbus.Variant
  63. }{
  64. V0: m.V0,
  65. V1: make(map[string]dbus.Variant, len(m.V1)),
  66. }
  67. for k, v := range m.V1 {
  68. p.V1[k] = v
  69. }
  70. properties = append(properties, p)
  71. }
  72. }
  73. return
  74. }
  75. // GetProperty is com.canonical.dbusmenu.GetProperty method.
  76. func (t *tray) GetProperty(id int32, name string) (value dbus.Variant, err *dbus.Error) {
  77. instance.menuLock.Lock()
  78. defer instance.menuLock.Unlock()
  79. if m, ok := findLayout(id); ok {
  80. if p, ok := m.V1[name]; ok {
  81. return p, nil
  82. }
  83. }
  84. return
  85. }
  86. // Event is com.canonical.dbusmenu.Event method.
  87. func (t *tray) Event(id int32, eventID string, data dbus.Variant, timestamp uint32) (err *dbus.Error) {
  88. if eventID == "clicked" {
  89. systrayMenuItemSelected(uint32(id))
  90. }
  91. return
  92. }
  93. // EventGroup is com.canonical.dbusmenu.EventGroup method.
  94. func (t *tray) EventGroup(events []struct {
  95. V0 int32
  96. V1 string
  97. V2 dbus.Variant
  98. V3 uint32
  99. }) (idErrors []int32, err *dbus.Error) {
  100. for _, event := range events {
  101. if event.V1 == "clicked" {
  102. systrayMenuItemSelected(uint32(event.V0))
  103. }
  104. }
  105. return
  106. }
  107. // AboutToShow is com.canonical.dbusmenu.AboutToShow method.
  108. func (t *tray) AboutToShow(id int32) (needUpdate bool, err *dbus.Error) {
  109. return
  110. }
  111. // AboutToShowGroup is com.canonical.dbusmenu.AboutToShowGroup method.
  112. func (t *tray) AboutToShowGroup(ids []int32) (updatesNeeded []int32, idErrors []int32, err *dbus.Error) {
  113. return
  114. }
  115. func createMenuPropSpec() map[string]map[string]*prop.Prop {
  116. instance.menuLock.Lock()
  117. defer instance.menuLock.Unlock()
  118. return map[string]map[string]*prop.Prop{
  119. "com.canonical.dbusmenu": {
  120. "Version": {
  121. Value: instance.menuVersion,
  122. Writable: true,
  123. Emit: prop.EmitTrue,
  124. Callback: nil,
  125. },
  126. "TextDirection": {
  127. Value: "ltr",
  128. Writable: false,
  129. Emit: prop.EmitTrue,
  130. Callback: nil,
  131. },
  132. "Status": {
  133. Value: "normal",
  134. Writable: false,
  135. Emit: prop.EmitTrue,
  136. Callback: nil,
  137. },
  138. "IconThemePath": {
  139. Value: []string{},
  140. Writable: false,
  141. Emit: prop.EmitTrue,
  142. Callback: nil,
  143. },
  144. },
  145. }
  146. }
  147. // menuLayout is a named struct to map into generated bindings. It represents the layout of a menu item
  148. type menuLayout = struct {
  149. V0 int32 // the unique ID of this item
  150. V1 map[string]dbus.Variant // properties for this menu item layout
  151. V2 []dbus.Variant // child menu item layouts
  152. }
  153. func addOrUpdateMenuItem(item *MenuItem) {
  154. var layout *menuLayout
  155. instance.menuLock.Lock()
  156. defer instance.menuLock.Unlock()
  157. m, exists := findLayout(int32(item.id))
  158. if exists {
  159. layout = m
  160. } else {
  161. layout = &menuLayout{
  162. V0: int32(item.id),
  163. V1: map[string]dbus.Variant{},
  164. V2: []dbus.Variant{},
  165. }
  166. parent := instance.menu
  167. if item.parent != nil {
  168. m, ok := findLayout(int32(item.parent.id))
  169. if ok {
  170. parent = m
  171. parent.V1["children-display"] = dbus.MakeVariant("submenu")
  172. }
  173. }
  174. parent.V2 = append(parent.V2, dbus.MakeVariant(layout))
  175. }
  176. applyItemToLayout(item, layout)
  177. if exists {
  178. refresh()
  179. }
  180. }
  181. func addSeparator(id uint32, parent uint32) {
  182. menu, _ := findLayout(int32(parent))
  183. instance.menuLock.Lock()
  184. defer instance.menuLock.Unlock()
  185. layout := &menuLayout{
  186. V0: int32(id),
  187. V1: map[string]dbus.Variant{
  188. "type": dbus.MakeVariant("separator"),
  189. },
  190. V2: []dbus.Variant{},
  191. }
  192. menu.V2 = append(menu.V2, dbus.MakeVariant(layout))
  193. refresh()
  194. }
  195. func applyItemToLayout(in *MenuItem, out *menuLayout) {
  196. out.V1["enabled"] = dbus.MakeVariant(!in.disabled)
  197. out.V1["label"] = dbus.MakeVariant(in.title)
  198. if in.isCheckable {
  199. out.V1["toggle-type"] = dbus.MakeVariant("checkmark")
  200. if in.checked {
  201. out.V1["toggle-state"] = dbus.MakeVariant(1)
  202. } else {
  203. out.V1["toggle-state"] = dbus.MakeVariant(0)
  204. }
  205. } else {
  206. out.V1["toggle-type"] = dbus.MakeVariant("")
  207. out.V1["toggle-state"] = dbus.MakeVariant(0)
  208. }
  209. }
  210. func findLayout(id int32) (*menuLayout, bool) {
  211. if id == 0 {
  212. return instance.menu, true
  213. }
  214. return findSubLayout(id, instance.menu.V2)
  215. }
  216. func findSubLayout(id int32, vals []dbus.Variant) (*menuLayout, bool) {
  217. for _, i := range vals {
  218. item := i.Value().(*menuLayout)
  219. if item.V0 == id {
  220. return item, true
  221. }
  222. if len(item.V2) > 0 {
  223. child, ok := findSubLayout(id, item.V2)
  224. if ok {
  225. return child, true
  226. }
  227. }
  228. }
  229. return nil, false
  230. }
  231. func removeSubLayout(id int32, vals []dbus.Variant) ([]dbus.Variant, bool) {
  232. for idx, i := range vals {
  233. item := i.Value().(*menuLayout)
  234. if item.V0 == id {
  235. return append(vals[:idx], vals[idx+1:]...), true
  236. }
  237. if len(item.V2) > 0 {
  238. if child, removed := removeSubLayout(id, item.V2); removed {
  239. return child, true
  240. }
  241. }
  242. }
  243. return vals, false
  244. }
  245. func removeMenuItem(item *MenuItem) {
  246. instance.menuLock.Lock()
  247. defer instance.menuLock.Unlock()
  248. parent := instance.menu
  249. if item.parent != nil {
  250. m, ok := findLayout(int32(item.parent.id))
  251. if !ok {
  252. return
  253. }
  254. parent = m
  255. }
  256. if items, removed := removeSubLayout(int32(item.id), parent.V2); removed {
  257. parent.V2 = items
  258. refresh()
  259. }
  260. }
  261. func hideMenuItem(item *MenuItem) {
  262. instance.menuLock.Lock()
  263. defer instance.menuLock.Unlock()
  264. m, exists := findLayout(int32(item.id))
  265. if exists {
  266. m.V1["visible"] = dbus.MakeVariant(false)
  267. refresh()
  268. }
  269. }
  270. func showMenuItem(item *MenuItem) {
  271. instance.menuLock.Lock()
  272. defer instance.menuLock.Unlock()
  273. m, exists := findLayout(int32(item.id))
  274. if exists {
  275. m.V1["visible"] = dbus.MakeVariant(true)
  276. refresh()
  277. }
  278. }
  279. func refresh() {
  280. if instance.conn == nil || instance.menuProps == nil {
  281. return
  282. }
  283. instance.menuVersion++
  284. dbusErr := instance.menuProps.Set("com.canonical.dbusmenu", "Version",
  285. dbus.MakeVariant(instance.menuVersion))
  286. if dbusErr != nil {
  287. log.Printf("systray error: failed to update menu version: %v\n", dbusErr)
  288. return
  289. }
  290. err := menu.Emit(instance.conn, &menu.Dbusmenu_LayoutUpdatedSignal{
  291. Path: menuPath,
  292. Body: &menu.Dbusmenu_LayoutUpdatedSignalBody{
  293. Revision: instance.menuVersion,
  294. },
  295. })
  296. if err != nil {
  297. log.Printf("systray error: failed to emit layout updated signal: %v\n", err)
  298. }
  299. }
  300. func resetMenu() {
  301. instance.menuLock.Lock()
  302. defer instance.menuLock.Unlock()
  303. instance.menu = &menuLayout{}
  304. instance.menuVersion++
  305. refresh()
  306. }