174 lines
5.6 KiB
CoffeeScript
174 lines
5.6 KiB
CoffeeScript
BrowserWindow = require 'browser-window'
|
|
EventEmitter = require('events').EventEmitter
|
|
MenuItem = require 'menu-item'
|
|
v8Util = process.atomBinding 'v8_util'
|
|
|
|
bindings = process.atomBinding 'menu'
|
|
|
|
# Automatically generated radio menu item's group id.
|
|
nextGroupId = 0
|
|
|
|
# Search between seperators to find a radio menu item and return its group id,
|
|
# otherwise generate a group id.
|
|
generateGroupId = (items, pos) ->
|
|
if pos > 0
|
|
for i in [pos - 1..0]
|
|
item = items[i]
|
|
return item.groupId if item.type is 'radio'
|
|
break if item.type is 'separator'
|
|
else if pos < items.length
|
|
for i in [pos..items.length - 1]
|
|
item = items[i]
|
|
return item.groupId if item.type is 'radio'
|
|
break if item.type is 'separator'
|
|
++nextGroupId
|
|
|
|
# Returns the index of item according to |id|.
|
|
indexOfItemById = (items, id) ->
|
|
return i for item, i in items when item.id is id
|
|
-1
|
|
|
|
# Returns the index of where to insert the item according to |position|.
|
|
indexToInsertByPosition = (items, position) ->
|
|
return items.length unless position
|
|
|
|
[query, id] = position.split '='
|
|
insertIndex = indexOfItemById items, id
|
|
if insertIndex is -1 and query isnt 'endof'
|
|
console.warn "Item with id '#{id}' is not found"
|
|
return items.length
|
|
|
|
switch query
|
|
when 'after'
|
|
insertIndex++
|
|
when 'endof'
|
|
# If the |id| doesn't exist, then create a new group with the |id|.
|
|
if insertIndex is -1
|
|
items.push id: id, type: 'separator'
|
|
insertIndex = items.length - 1
|
|
|
|
# Find the end of the group.
|
|
insertIndex++
|
|
while insertIndex < items.length and items[insertIndex].type isnt 'separator'
|
|
insertIndex++
|
|
|
|
insertIndex
|
|
|
|
Menu = bindings.Menu
|
|
Menu::__proto__ = EventEmitter.prototype
|
|
|
|
Menu::_init = ->
|
|
@commandsMap = {}
|
|
@groupsMap = {}
|
|
@items = []
|
|
@delegate =
|
|
isCommandIdChecked: (commandId) => @commandsMap[commandId]?.checked
|
|
isCommandIdEnabled: (commandId) => @commandsMap[commandId]?.enabled
|
|
isCommandIdVisible: (commandId) => @commandsMap[commandId]?.visible
|
|
getAcceleratorForCommandId: (commandId) => @commandsMap[commandId]?.accelerator
|
|
getIconForCommandId: (commandId) => @commandsMap[commandId]?.icon
|
|
executeCommand: (commandId) => @commandsMap[commandId]?.click()
|
|
menuWillShow: =>
|
|
# Make sure radio groups have at least one menu item seleted.
|
|
for id, group of @groupsMap
|
|
checked = false
|
|
for radioItem in group when radioItem.checked
|
|
checked = true
|
|
break
|
|
v8Util.setHiddenValue group[0], 'checked', true unless checked
|
|
|
|
Menu::popup = (window, x, y) ->
|
|
throw new TypeError('Invalid window') unless window?.constructor is BrowserWindow
|
|
if x? and y?
|
|
@_popupAt(window, x, y)
|
|
else
|
|
@_popup window
|
|
|
|
Menu::append = (item) ->
|
|
@insert @getItemCount(), item
|
|
|
|
Menu::insert = (pos, item) ->
|
|
throw new TypeError('Invalid item') unless item?.constructor is MenuItem
|
|
|
|
switch item.type
|
|
when 'normal' then @insertItem pos, item.commandId, item.label
|
|
when 'checkbox' then @insertCheckItem pos, item.commandId, item.label
|
|
when 'separator' then @insertSeparator pos
|
|
when 'submenu' then @insertSubMenu pos, item.commandId, item.label, item.submenu
|
|
when 'radio'
|
|
# Grouping radio menu items.
|
|
item.overrideReadOnlyProperty 'groupId', generateGroupId(@items, pos)
|
|
@groupsMap[item.groupId] ?= []
|
|
@groupsMap[item.groupId].push item
|
|
|
|
# Setting a radio menu item should flip other items in the group.
|
|
v8Util.setHiddenValue item, 'checked', item.checked
|
|
Object.defineProperty item, 'checked',
|
|
enumerable: true
|
|
get: -> v8Util.getHiddenValue item, 'checked'
|
|
set: (val) =>
|
|
for otherItem in @groupsMap[item.groupId] when otherItem isnt item
|
|
v8Util.setHiddenValue otherItem, 'checked', false
|
|
v8Util.setHiddenValue item, 'checked', true
|
|
|
|
@insertRadioItem pos, item.commandId, item.label, item.groupId
|
|
|
|
@setSublabel pos, item.sublabel if item.sublabel?
|
|
@setIcon pos, item.icon if item.icon?
|
|
|
|
# Make menu accessable to items.
|
|
item.overrideReadOnlyProperty 'menu', this
|
|
|
|
# Remember the items.
|
|
@items.splice pos, 0, item
|
|
@commandsMap[item.commandId] = item
|
|
|
|
# Force menuWillShow to be called
|
|
Menu::_callMenuWillShow = ->
|
|
@delegate?.menuWillShow()
|
|
item.submenu._callMenuWillShow() for item in @items when item.submenu?
|
|
|
|
applicationMenu = null
|
|
Menu.setApplicationMenu = (menu) ->
|
|
throw new TypeError('Invalid menu') unless menu is null or menu.constructor is Menu
|
|
applicationMenu = menu # Keep a reference.
|
|
|
|
if process.platform is 'darwin'
|
|
return if menu is null
|
|
menu._callMenuWillShow()
|
|
bindings.setApplicationMenu menu
|
|
else
|
|
windows = BrowserWindow.getAllWindows()
|
|
w.setMenu menu for w in windows
|
|
|
|
Menu.getApplicationMenu = -> applicationMenu
|
|
|
|
Menu.sendActionToFirstResponder = bindings.sendActionToFirstResponder
|
|
|
|
Menu.buildFromTemplate = (template) ->
|
|
throw new TypeError('Invalid template for Menu') unless Array.isArray template
|
|
|
|
positionedTemplate = []
|
|
insertIndex = 0
|
|
|
|
for item in template
|
|
if item.position
|
|
insertIndex = indexToInsertByPosition positionedTemplate, item.position
|
|
else
|
|
# If no |position| is specified, insert after last item.
|
|
insertIndex++
|
|
positionedTemplate.splice insertIndex, 0, item
|
|
|
|
menu = new Menu
|
|
|
|
for item in positionedTemplate
|
|
throw new TypeError('Invalid template for MenuItem') unless typeof item is 'object'
|
|
|
|
item.submenu = Menu.buildFromTemplate item.submenu if item.submenu?
|
|
menuItem = new MenuItem(item)
|
|
menuItem[key] = value for key, value of item when not menuItem[key]?
|
|
menu.append menuItem
|
|
|
|
menu
|
|
|
|
module.exports = Menu
|