menu_darwin.go 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. //go:build !no_native_menus && !js && !wasm && !test_web_driver
  2. // +build !no_native_menus,!js,!wasm,!test_web_driver
  3. package glfw
  4. import (
  5. "bytes"
  6. "fmt"
  7. "image/color"
  8. "image/png"
  9. "strings"
  10. "unsafe"
  11. "fyne.io/fyne/v2"
  12. "fyne.io/fyne/v2/canvas"
  13. "fyne.io/fyne/v2/internal/painter"
  14. "fyne.io/fyne/v2/internal/svg"
  15. "fyne.io/fyne/v2/theme"
  16. )
  17. /*
  18. #cgo CFLAGS: -x objective-c
  19. #cgo LDFLAGS: -framework Foundation -framework AppKit
  20. #include <AppKit/AppKit.h>
  21. // Using void* as type for pointers is a workaround. See https://github.com/golang/go/issues/12065.
  22. void assignDarwinSubmenu(const void*, const void*);
  23. void completeDarwinMenu(void* menu, bool prepend);
  24. const void* createDarwinMenu(const char* label);
  25. const void* darwinAppMenu();
  26. void getTextColorRGBA(int* r, int* g, int* b, int* a);
  27. const void* insertDarwinMenuItem(const void* menu, const char* label, const char* keyEquivalent, unsigned int keyEquivalentModifierMask, int id, int index, bool isSeparator, const void *imageData, unsigned int imageDataLength);
  28. int menuFontSize();
  29. void resetDarwinMenu();
  30. // Used for tests.
  31. const void* test_darwinMainMenu();
  32. const void* test_NSMenu_itemAtIndex(const void*, NSInteger);
  33. NSInteger test_NSMenu_numberOfItems(const void*);
  34. void test_NSMenu_performActionForItemAtIndex(const void*, NSInteger);
  35. void test_NSMenu_removeItemAtIndex(const void* m, NSInteger i);
  36. const char* test_NSMenu_title(const void*);
  37. bool test_NSMenuItem_isSeparatorItem(const void*);
  38. const char* test_NSMenuItem_keyEquivalent(const void*);
  39. unsigned long test_NSMenuItem_keyEquivalentModifierMask(const void*);
  40. const void* test_NSMenuItem_submenu(const void*);
  41. const char* test_NSMenuItem_title(const void*);
  42. */
  43. import "C"
  44. type menuCallbacks struct {
  45. action func()
  46. enabled func() bool
  47. checked func() bool
  48. }
  49. var callbacks []*menuCallbacks
  50. var ecb func(string)
  51. var specialKeys = map[fyne.KeyName]string{
  52. fyne.KeyBackspace: "\x08",
  53. fyne.KeyDelete: "\x7f",
  54. fyne.KeyDown: "\uf701",
  55. fyne.KeyEnd: "\uf72b",
  56. fyne.KeyEnter: "\x03",
  57. fyne.KeyEscape: "\x1b",
  58. fyne.KeyF10: "\uf70d",
  59. fyne.KeyF11: "\uf70e",
  60. fyne.KeyF12: "\uf70f",
  61. fyne.KeyF1: "\uf704",
  62. fyne.KeyF2: "\uf705",
  63. fyne.KeyF3: "\uf706",
  64. fyne.KeyF4: "\uf707",
  65. fyne.KeyF5: "\uf708",
  66. fyne.KeyF6: "\uf709",
  67. fyne.KeyF7: "\uf70a",
  68. fyne.KeyF8: "\uf70b",
  69. fyne.KeyF9: "\uf70c",
  70. fyne.KeyHome: "\uf729",
  71. fyne.KeyInsert: "\uf727",
  72. fyne.KeyLeft: "\uf702",
  73. fyne.KeyPageDown: "\uf72d",
  74. fyne.KeyPageUp: "\uf72c",
  75. fyne.KeyReturn: "\n",
  76. fyne.KeyRight: "\uf703",
  77. fyne.KeySpace: " ",
  78. fyne.KeyTab: "\t",
  79. fyne.KeyUp: "\uf700",
  80. }
  81. func addNativeMenu(w *window, menu *fyne.Menu, nextItemID int, prepend bool) int {
  82. menu, nextItemID = handleSpecialItems(w, menu, nextItemID, true)
  83. containsItems := false
  84. for _, item := range menu.Items {
  85. if !item.IsSeparator {
  86. containsItems = true
  87. break
  88. }
  89. }
  90. if !containsItems {
  91. return nextItemID
  92. }
  93. nsMenu, nextItemID := createNativeMenu(w, menu, nextItemID)
  94. C.completeDarwinMenu(nsMenu, C.bool(prepend))
  95. return nextItemID
  96. }
  97. func addNativeSubmenu(w *window, nsParentMenuItem unsafe.Pointer, menu *fyne.Menu, nextItemID int) int {
  98. nsMenu, nextItemID := createNativeMenu(w, menu, nextItemID)
  99. C.assignDarwinSubmenu(nsParentMenuItem, nsMenu)
  100. return nextItemID
  101. }
  102. func clearNativeMenu() {
  103. C.resetDarwinMenu()
  104. }
  105. func createNativeMenu(w *window, menu *fyne.Menu, nextItemID int) (unsafe.Pointer, int) {
  106. nsMenu := C.createDarwinMenu(C.CString(menu.Label))
  107. for _, item := range menu.Items {
  108. nsMenuItem := insertNativeMenuItem(nsMenu, item, nextItemID, -1)
  109. nextItemID = registerCallback(w, item, nextItemID)
  110. if item.ChildMenu != nil {
  111. nextItemID = addNativeSubmenu(w, nsMenuItem, item.ChildMenu, nextItemID)
  112. }
  113. }
  114. return nsMenu, nextItemID
  115. }
  116. //export exceptionCallback
  117. func exceptionCallback(e *C.char) {
  118. msg := C.GoString(e)
  119. if ecb == nil {
  120. panic("unhandled Obj-C exception: " + msg)
  121. }
  122. ecb(msg)
  123. }
  124. func handleSpecialItems(w *window, menu *fyne.Menu, nextItemID int, addSeparator bool) (*fyne.Menu, int) {
  125. for i, item := range menu.Items {
  126. if item.Label == "Settings" || item.Label == "Settings…" || item.Label == "Preferences" || item.Label == "Preferences…" {
  127. items := make([]*fyne.MenuItem, 0, len(menu.Items)-1)
  128. items = append(items, menu.Items[:i]...)
  129. items = append(items, menu.Items[i+1:]...)
  130. menu, nextItemID = handleSpecialItems(w, fyne.NewMenu(menu.Label, items...), nextItemID, false)
  131. insertNativeMenuItem(C.darwinAppMenu(), item, nextItemID, 1)
  132. if addSeparator {
  133. C.insertDarwinMenuItem(
  134. C.darwinAppMenu(),
  135. C.CString(""),
  136. C.CString(""),
  137. C.uint(0),
  138. C.int(nextItemID),
  139. C.int(1),
  140. C.bool(true),
  141. unsafe.Pointer(nil),
  142. C.uint(0),
  143. )
  144. }
  145. nextItemID = registerCallback(w, item, nextItemID)
  146. break
  147. }
  148. }
  149. return menu, nextItemID
  150. }
  151. // TODO: theme change support, see NSSystemColorsDidChangeNotification
  152. func insertNativeMenuItem(nsMenu unsafe.Pointer, item *fyne.MenuItem, nextItemID, index int) unsafe.Pointer {
  153. var imgData unsafe.Pointer
  154. var imgDataLength uint
  155. if item.Icon != nil {
  156. if svg.IsResourceSVG(item.Icon) {
  157. rsc := item.Icon
  158. if _, isThemed := rsc.(*theme.ThemedResource); isThemed {
  159. var r, g, b, a C.int
  160. C.getTextColorRGBA(&r, &g, &b, &a)
  161. rsc = &fyne.StaticResource{
  162. StaticName: rsc.Name(),
  163. StaticContent: svg.Colorize(rsc.Content(), color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: uint8(a)}),
  164. }
  165. }
  166. size := int(C.menuFontSize())
  167. img := painter.PaintImage(&canvas.Image{Resource: rsc}, nil, size, size)
  168. var buf bytes.Buffer
  169. if err := png.Encode(&buf, img); err != nil {
  170. fyne.LogError("failed to render menu icon", err)
  171. } else {
  172. imgData = unsafe.Pointer(&buf.Bytes()[0])
  173. imgDataLength = uint(buf.Len())
  174. }
  175. } else {
  176. imgData = unsafe.Pointer(&item.Icon.Content()[0])
  177. imgDataLength = uint(len(item.Icon.Content()))
  178. }
  179. }
  180. return C.insertDarwinMenuItem(
  181. nsMenu,
  182. C.CString(item.Label),
  183. C.CString(keyEquivalent(item)),
  184. C.uint(keyEquivalentModifierMask(item)),
  185. C.int(nextItemID),
  186. C.int(index),
  187. C.bool(item.IsSeparator),
  188. imgData,
  189. C.uint(imgDataLength),
  190. )
  191. }
  192. func keyEquivalent(item *fyne.MenuItem) (key string) {
  193. if s, ok := item.Shortcut.(fyne.KeyboardShortcut); ok {
  194. if key = specialKeys[s.Key()]; key == "" {
  195. if len(s.Key()) > 1 {
  196. fyne.LogError(fmt.Sprintf("unsupported key “%s” for menu shortcut", s.Key()), nil)
  197. }
  198. key = strings.ToLower(string(s.Key()))
  199. }
  200. }
  201. return
  202. }
  203. func keyEquivalentModifierMask(item *fyne.MenuItem) (mask uint) {
  204. if s, ok := item.Shortcut.(fyne.KeyboardShortcut); ok {
  205. if (s.Mod() & fyne.KeyModifierShift) != 0 {
  206. mask |= 1 << 17 // NSEventModifierFlagShift
  207. }
  208. if (s.Mod() & fyne.KeyModifierAlt) != 0 {
  209. mask |= 1 << 19 // NSEventModifierFlagOption
  210. }
  211. if (s.Mod() & fyne.KeyModifierControl) != 0 {
  212. mask |= 1 << 18 // NSEventModifierFlagControl
  213. }
  214. if (s.Mod() & fyne.KeyModifierSuper) != 0 {
  215. mask |= 1 << 20 // NSEventModifierFlagCommand
  216. }
  217. }
  218. return
  219. }
  220. func registerCallback(w *window, item *fyne.MenuItem, nextItemID int) int {
  221. if !item.IsSeparator {
  222. callbacks = append(callbacks, &menuCallbacks{
  223. action: func() {
  224. if item.Action != nil {
  225. w.QueueEvent(item.Action)
  226. }
  227. },
  228. enabled: func() bool {
  229. return !item.Disabled
  230. },
  231. checked: func() bool {
  232. return item.Checked
  233. },
  234. })
  235. nextItemID++
  236. }
  237. return nextItemID
  238. }
  239. func setExceptionCallback(cb func(string)) {
  240. ecb = cb
  241. }
  242. func hasNativeMenu() bool {
  243. return true
  244. }
  245. //export menuCallback
  246. func menuCallback(id int) {
  247. callbacks[id].action()
  248. }
  249. //export menuEnabled
  250. func menuEnabled(id int) bool {
  251. return callbacks[id].enabled()
  252. }
  253. //export menuChecked
  254. func menuChecked(id int) bool {
  255. return callbacks[id].checked()
  256. }
  257. func setupNativeMenu(w *window, main *fyne.MainMenu) {
  258. clearNativeMenu()
  259. nextItemID := 0
  260. callbacks = []*menuCallbacks{}
  261. var helpMenu *fyne.Menu
  262. for i := len(main.Items) - 1; i >= 0; i-- {
  263. menu := main.Items[i]
  264. if menu.Label == "Help" {
  265. helpMenu = menu
  266. continue
  267. }
  268. nextItemID = addNativeMenu(w, menu, nextItemID, true)
  269. }
  270. if helpMenu != nil {
  271. addNativeMenu(w, helpMenu, nextItemID, false)
  272. }
  273. }
  274. //
  275. // Test support methods
  276. // These are needed because CGo is not supported inside test files.
  277. //
  278. func testDarwinMainMenu() unsafe.Pointer {
  279. return C.test_darwinMainMenu()
  280. }
  281. func testNSMenuItemAtIndex(m unsafe.Pointer, i int) unsafe.Pointer {
  282. return C.test_NSMenu_itemAtIndex(m, C.long(i))
  283. }
  284. func testNSMenuNumberOfItems(m unsafe.Pointer) int {
  285. return int(C.test_NSMenu_numberOfItems(m))
  286. }
  287. func testNSMenuPerformActionForItemAtIndex(m unsafe.Pointer, i int) {
  288. C.test_NSMenu_performActionForItemAtIndex(m, C.long(i))
  289. }
  290. func testNSMenuRemoveItemAtIndex(m unsafe.Pointer, i int) {
  291. C.test_NSMenu_removeItemAtIndex(m, C.long(i))
  292. }
  293. func testNSMenuTitle(m unsafe.Pointer) string {
  294. return C.GoString(C.test_NSMenu_title(m))
  295. }
  296. func testNSMenuItemIsSeparatorItem(i unsafe.Pointer) bool {
  297. return bool(C.test_NSMenuItem_isSeparatorItem(i))
  298. }
  299. func testNSMenuItemKeyEquivalent(i unsafe.Pointer) string {
  300. return C.GoString(C.test_NSMenuItem_keyEquivalent(i))
  301. }
  302. func testNSMenuItemKeyEquivalentModifierMask(i unsafe.Pointer) uint64 {
  303. return uint64(C.ulong(C.test_NSMenuItem_keyEquivalentModifierMask(i)))
  304. }
  305. func testNSMenuItemSubmenu(i unsafe.Pointer) unsafe.Pointer {
  306. return C.test_NSMenuItem_submenu(i)
  307. }
  308. func testNSMenuItemTitle(i unsafe.Pointer) string {
  309. return C.GoString(C.test_NSMenuItem_title(i))
  310. }