menu.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. 'use strict'
  2. const {BrowserWindow, MenuItem, webContents} = require('electron')
  3. const EventEmitter = require('events').EventEmitter
  4. const v8Util = process.atomBinding('v8_util')
  5. const bindings = process.atomBinding('menu')
  6. // Automatically generated radio menu item's group id.
  7. var nextGroupId = 0
  8. // Search between separators to find a radio menu item and return its group id,
  9. // otherwise generate a group id.
  10. var generateGroupId = function (items, pos) {
  11. var i, item, j, k, ref1, ref2, ref3
  12. if (pos > 0) {
  13. for (i = j = ref1 = pos - 1; ref1 <= 0 ? j <= 0 : j >= 0; i = ref1 <= 0 ? ++j : --j) {
  14. item = items[i]
  15. if (item.type === 'radio') {
  16. return item.groupId
  17. }
  18. if (item.type === 'separator') {
  19. break
  20. }
  21. }
  22. } else if (pos < items.length) {
  23. for (i = k = ref2 = pos, ref3 = items.length - 1; ref2 <= ref3 ? k <= ref3 : k >= ref3; i = ref2 <= ref3 ? ++k : --k) {
  24. item = items[i]
  25. if (item.type === 'radio') {
  26. return item.groupId
  27. }
  28. if (item.type === 'separator') {
  29. break
  30. }
  31. }
  32. }
  33. return ++nextGroupId
  34. }
  35. // Returns the index of item according to |id|.
  36. var indexOfItemById = function (items, id) {
  37. var i, item, j, len
  38. for (i = j = 0, len = items.length; j < len; i = ++j) {
  39. item = items[i]
  40. if (item.id === id) {
  41. return i
  42. }
  43. }
  44. return -1
  45. }
  46. // Returns the index of where to insert the item according to |position|.
  47. var indexToInsertByPosition = function (items, position) {
  48. var insertIndex
  49. if (!position) {
  50. return items.length
  51. }
  52. const [query, id] = position.split('=')
  53. insertIndex = indexOfItemById(items, id)
  54. if (insertIndex === -1 && query !== 'endof') {
  55. console.warn("Item with id '" + id + "' is not found")
  56. return items.length
  57. }
  58. switch (query) {
  59. case 'after':
  60. insertIndex++
  61. break
  62. case 'endof':
  63. // If the |id| doesn't exist, then create a new group with the |id|.
  64. if (insertIndex === -1) {
  65. items.push({
  66. id: id,
  67. type: 'separator'
  68. })
  69. insertIndex = items.length - 1
  70. }
  71. // Find the end of the group.
  72. insertIndex++
  73. while (insertIndex < items.length && items[insertIndex].type !== 'separator') {
  74. insertIndex++
  75. }
  76. }
  77. return insertIndex
  78. }
  79. const Menu = bindings.Menu
  80. Object.setPrototypeOf(Menu.prototype, EventEmitter.prototype)
  81. Menu.prototype._init = function () {
  82. this.commandsMap = {}
  83. this.groupsMap = {}
  84. this.items = []
  85. this.delegate = {
  86. isCommandIdChecked: (commandId) => {
  87. var command = this.commandsMap[commandId]
  88. return command != null ? command.checked : undefined
  89. },
  90. isCommandIdEnabled: (commandId) => {
  91. var command = this.commandsMap[commandId]
  92. return command != null ? command.enabled : undefined
  93. },
  94. isCommandIdVisible: (commandId) => {
  95. var command = this.commandsMap[commandId]
  96. return command != null ? command.visible : undefined
  97. },
  98. getAcceleratorForCommandId: (commandId, useDefaultAccelerator) => {
  99. const command = this.commandsMap[commandId]
  100. if (command == null) return
  101. if (command.accelerator != null) return command.accelerator
  102. if (useDefaultAccelerator) return command.getDefaultRoleAccelerator()
  103. },
  104. getIconForCommandId: (commandId) => {
  105. var command = this.commandsMap[commandId]
  106. return command != null ? command.icon : undefined
  107. },
  108. executeCommand: (event, commandId) => {
  109. const command = this.commandsMap[commandId]
  110. if (command == null) return
  111. command.click(event, BrowserWindow.getFocusedWindow(), webContents.getFocusedWebContents())
  112. },
  113. menuWillShow: () => {
  114. // Make sure radio groups have at least one menu item seleted.
  115. var checked, group, id, j, len, radioItem, ref1
  116. ref1 = this.groupsMap
  117. for (id in ref1) {
  118. group = ref1[id]
  119. checked = false
  120. for (j = 0, len = group.length; j < len; j++) {
  121. radioItem = group[j]
  122. if (!radioItem.checked) {
  123. continue
  124. }
  125. checked = true
  126. break
  127. }
  128. if (!checked) {
  129. v8Util.setHiddenValue(group[0], 'checked', true)
  130. }
  131. }
  132. }
  133. }
  134. }
  135. Menu.prototype.popup = function (window, x, y, positioningItem) {
  136. let asyncPopup
  137. // menu.popup(x, y, positioningItem)
  138. if (window != null && (typeof window !== 'object' || window.constructor !== BrowserWindow)) {
  139. // Shift.
  140. positioningItem = y
  141. y = x
  142. x = window
  143. window = null
  144. }
  145. // menu.popup(window, {})
  146. if (x != null && typeof x === 'object') {
  147. const options = x
  148. x = options.x
  149. y = options.y
  150. positioningItem = options.positioningItem
  151. asyncPopup = options.async
  152. }
  153. // Default to showing in focused window.
  154. if (window == null) window = BrowserWindow.getFocusedWindow()
  155. // Default to showing under mouse location.
  156. if (typeof x !== 'number') x = -1
  157. if (typeof y !== 'number') y = -1
  158. // Default to not highlighting any item.
  159. if (typeof positioningItem !== 'number') positioningItem = -1
  160. // Default to synchronous for backwards compatibility.
  161. if (typeof asyncPopup !== 'boolean') asyncPopup = false
  162. this.popupAt(window, x, y, positioningItem, asyncPopup)
  163. }
  164. Menu.prototype.closePopup = function (window) {
  165. if (window == null || window.constructor !== BrowserWindow) {
  166. window = BrowserWindow.getFocusedWindow()
  167. }
  168. if (window != null) {
  169. this.closePopupAt(window.id)
  170. }
  171. }
  172. Menu.prototype.append = function (item) {
  173. return this.insert(this.getItemCount(), item)
  174. }
  175. Menu.prototype.insert = function (pos, item) {
  176. var base, name
  177. if ((item != null ? item.constructor : void 0) !== MenuItem) {
  178. throw new TypeError('Invalid item')
  179. }
  180. switch (item.type) {
  181. case 'normal':
  182. this.insertItem(pos, item.commandId, item.label)
  183. break
  184. case 'checkbox':
  185. this.insertCheckItem(pos, item.commandId, item.label)
  186. break
  187. case 'separator':
  188. this.insertSeparator(pos)
  189. break
  190. case 'submenu':
  191. this.insertSubMenu(pos, item.commandId, item.label, item.submenu)
  192. break
  193. case 'radio':
  194. // Grouping radio menu items.
  195. item.overrideReadOnlyProperty('groupId', generateGroupId(this.items, pos))
  196. if ((base = this.groupsMap)[name = item.groupId] == null) {
  197. base[name] = []
  198. }
  199. this.groupsMap[item.groupId].push(item)
  200. // Setting a radio menu item should flip other items in the group.
  201. v8Util.setHiddenValue(item, 'checked', item.checked)
  202. Object.defineProperty(item, 'checked', {
  203. enumerable: true,
  204. get: function () {
  205. return v8Util.getHiddenValue(item, 'checked')
  206. },
  207. set: () => {
  208. var j, len, otherItem, ref1
  209. ref1 = this.groupsMap[item.groupId]
  210. for (j = 0, len = ref1.length; j < len; j++) {
  211. otherItem = ref1[j]
  212. if (otherItem !== item) {
  213. v8Util.setHiddenValue(otherItem, 'checked', false)
  214. }
  215. }
  216. return v8Util.setHiddenValue(item, 'checked', true)
  217. }
  218. })
  219. this.insertRadioItem(pos, item.commandId, item.label, item.groupId)
  220. }
  221. if (item.sublabel != null) {
  222. this.setSublabel(pos, item.sublabel)
  223. }
  224. if (item.icon != null) {
  225. this.setIcon(pos, item.icon)
  226. }
  227. if (item.role != null) {
  228. this.setRole(pos, item.role)
  229. }
  230. // Make menu accessable to items.
  231. item.overrideReadOnlyProperty('menu', this)
  232. // Remember the items.
  233. this.items.splice(pos, 0, item)
  234. this.commandsMap[item.commandId] = item
  235. }
  236. // Force menuWillShow to be called
  237. Menu.prototype._callMenuWillShow = function () {
  238. if (this.delegate != null) {
  239. this.delegate.menuWillShow()
  240. }
  241. this.items.forEach(function (item) {
  242. if (item.submenu != null) {
  243. item.submenu._callMenuWillShow()
  244. }
  245. })
  246. }
  247. var applicationMenu = null
  248. Menu.setApplicationMenu = function (menu) {
  249. if (!(menu === null || menu.constructor === Menu)) {
  250. throw new TypeError('Invalid menu')
  251. }
  252. // Keep a reference.
  253. applicationMenu = menu
  254. if (process.platform === 'darwin') {
  255. if (menu === null) {
  256. return
  257. }
  258. menu._callMenuWillShow()
  259. bindings.setApplicationMenu(menu)
  260. } else {
  261. BrowserWindow.getAllWindows().forEach(function (window) {
  262. window.setMenu(menu)
  263. })
  264. }
  265. }
  266. Menu.getApplicationMenu = function () {
  267. return applicationMenu
  268. }
  269. Menu.sendActionToFirstResponder = bindings.sendActionToFirstResponder
  270. Menu.buildFromTemplate = function (template) {
  271. var insertIndex, item, j, k, len, len1, menu, menuItem, positionedTemplate
  272. if (!Array.isArray(template)) {
  273. throw new TypeError('Invalid template for Menu')
  274. }
  275. positionedTemplate = []
  276. insertIndex = 0
  277. for (j = 0, len = template.length; j < len; j++) {
  278. item = template[j]
  279. if (item.position) {
  280. insertIndex = indexToInsertByPosition(positionedTemplate, item.position)
  281. } else {
  282. // If no |position| is specified, insert after last item.
  283. insertIndex++
  284. }
  285. positionedTemplate.splice(insertIndex, 0, item)
  286. }
  287. menu = new Menu()
  288. for (k = 0, len1 = positionedTemplate.length; k < len1; k++) {
  289. item = positionedTemplate[k]
  290. if (typeof item !== 'object') {
  291. throw new TypeError('Invalid template for MenuItem')
  292. }
  293. menuItem = new MenuItem(item)
  294. menu.append(menuItem)
  295. }
  296. return menu
  297. }
  298. module.exports = Menu