mirror of
https://github.com/atom/atom.git
synced 2026-01-24 06:18:03 -05:00
When only a modifier is pressed it still triggers a keydown event. This normalizes the keystroke name for those events.
230 lines
7.6 KiB
CoffeeScript
230 lines
7.6 KiB
CoffeeScript
{$} = require './space-pen-extensions'
|
|
_ = require 'underscore-plus'
|
|
fsUtils = require './fs-utils'
|
|
path = require 'path'
|
|
CSON = require 'season'
|
|
BindingSet = require './binding-set'
|
|
{Emitter} = require 'emissary'
|
|
|
|
Modifiers = ['alt', 'control', 'ctrl', 'shift', 'meta']
|
|
|
|
# Internal: Associates keymaps with actions.
|
|
#
|
|
# Keymaps are defined in a CSON format. A typical keymap looks something like this:
|
|
#
|
|
# ```cson
|
|
# 'body':
|
|
# 'ctrl-l': 'package:do-something'
|
|
#'.someClass':
|
|
# 'enter': 'package:confirm'
|
|
# ```
|
|
#
|
|
# As a key, you define the DOM element you want to work on, using CSS notation. For that
|
|
# key, you define one or more key:value pairs, associating keystrokes with a command to execute.
|
|
module.exports =
|
|
class Keymap
|
|
Emitter.includeInto(this)
|
|
|
|
bindingSets: null
|
|
nextBindingSetIndex: 0
|
|
bindingSetsByFirstKeystroke: null
|
|
queuedKeystrokes: null
|
|
|
|
constructor: ->
|
|
@bindingSets = []
|
|
@bindingSetsByFirstKeystroke = {}
|
|
|
|
loadBundledKeymaps: ->
|
|
@loadDirectory(config.bundledKeymapsDirPath)
|
|
@emit('bundled-keymaps-loaded')
|
|
|
|
loadUserKeymap: ->
|
|
userKeymapPath = CSON.resolve(path.join(config.configDirPath, 'keymap'))
|
|
@load(userKeymapPath) if userKeymapPath
|
|
|
|
loadDirectory: (directoryPath) ->
|
|
@load(filePath) for filePath in fsUtils.listSync(directoryPath, ['.cson', '.json'])
|
|
|
|
load: (path) ->
|
|
@add(path, CSON.readFileSync(path))
|
|
|
|
add: (args...) ->
|
|
name = args.shift() if args.length > 1
|
|
keymap = args.shift()
|
|
for selector, bindings of keymap
|
|
@bindKeys(name, selector, bindings)
|
|
|
|
remove: (name) ->
|
|
for bindingSet in @bindingSets.filter((bindingSet) -> bindingSet.name is name)
|
|
_.remove(@bindingSets, bindingSet)
|
|
for keystrokes of bindingSet.commandsByKeystrokes
|
|
keystroke = keystrokes.split(' ')[0]
|
|
_.remove(@bindingSetsByFirstKeystroke[keystroke], bindingSet)
|
|
|
|
# Public: Returns an array of objects that represent every keystroke to
|
|
# command mapping. Each object contains the following keys `source`,
|
|
# `selector`, `command`, `keystrokes`.
|
|
getAllKeyMappings: ->
|
|
mappings = []
|
|
for bindingSet in @bindingSets
|
|
selector = bindingSet.getSelector()
|
|
source = @determineSource(bindingSet.getName())
|
|
for keystrokes, command of bindingSet.getCommandsByKeystrokes()
|
|
mappings.push {keystrokes, command, selector, source}
|
|
|
|
mappings
|
|
|
|
# Private: Returns a user friendly description of where a keybinding was
|
|
# loaded from.
|
|
#
|
|
# * filePath:
|
|
# The absolute path from which the keymap was loaded
|
|
#
|
|
# Returns one of:
|
|
# * `Core` indicates it comes from a bundled package.
|
|
# * `User` indicates that it was defined by a user.
|
|
# * `<package-name>` the package which defined it.
|
|
# * `Unknown` if an invalid path was passed in.
|
|
determineSource: (filePath) ->
|
|
return 'Unknown' unless filePath
|
|
|
|
pathParts = filePath.split(path.sep)
|
|
if _.contains(pathParts, 'node_modules') or _.contains(pathParts, 'atom') or _.contains(pathParts, 'src')
|
|
'Core'
|
|
else if _.contains(pathParts, '.atom') and _.contains(pathParts, 'keymaps') and !_.contains(pathParts, 'packages')
|
|
'User'
|
|
else
|
|
packageNameIndex = pathParts.length - 3
|
|
pathParts[packageNameIndex]
|
|
|
|
bindKeys: (args...) ->
|
|
name = args.shift() if args.length > 2
|
|
[selector, bindings] = args
|
|
bindingSet = new BindingSet(selector, bindings, @nextBindingSetIndex++, name)
|
|
@bindingSets.unshift(bindingSet)
|
|
for keystrokes of bindingSet.commandsByKeystrokes
|
|
keystroke = keystrokes.split(' ')[0] # only index by first keystroke
|
|
@bindingSetsByFirstKeystroke[keystroke] ?= []
|
|
@bindingSetsByFirstKeystroke[keystroke].push(bindingSet)
|
|
|
|
unbindKeys: (selector, bindings) ->
|
|
bindingSet = _.detect @bindingSets, (bindingSet) ->
|
|
bindingSet.selector is selector and bindingSet.bindings is bindings
|
|
|
|
if bindingSet
|
|
_.remove(@bindingSets, bindingSet)
|
|
|
|
bindingsForElement: (element) ->
|
|
keystrokeMap = {}
|
|
currentNode = $(element)
|
|
|
|
while currentNode.length
|
|
bindingSets = @bindingSetsForNode(currentNode)
|
|
_.defaults(keystrokeMap, set.commandsByKeystrokes) for set in bindingSets
|
|
currentNode = currentNode.parent()
|
|
|
|
keystrokeMap
|
|
|
|
handleKeyEvent: (event) =>
|
|
event.keystrokes = @multiKeystrokeStringForEvent(event)
|
|
isMultiKeystroke = @queuedKeystrokes?
|
|
@queuedKeystrokes = null
|
|
|
|
firstKeystroke = event.keystrokes.split(' ')[0]
|
|
bindingSetsForFirstKeystroke = @bindingSetsByFirstKeystroke[firstKeystroke]
|
|
if bindingSetsForFirstKeystroke?
|
|
currentNode = $(event.target)
|
|
currentNode = rootView if currentNode is $('body')[0]
|
|
while currentNode.length
|
|
candidateBindingSets = @bindingSetsForNode(currentNode, bindingSetsForFirstKeystroke)
|
|
for bindingSet in candidateBindingSets
|
|
command = bindingSet.commandForEvent(event)
|
|
if command is 'native!'
|
|
return true
|
|
else if command
|
|
continue if @triggerCommandEvent(event, command)
|
|
return false
|
|
else if command == false
|
|
return false
|
|
|
|
if bindingSet.matchesKeystrokePrefix(event)
|
|
@queuedKeystrokes = event.keystrokes
|
|
return false
|
|
currentNode = currentNode.parent()
|
|
|
|
return false if isMultiKeystroke
|
|
return false if firstKeystroke is 'tab'
|
|
|
|
bindingSetsForNode: (node, candidateBindingSets = @bindingSets) ->
|
|
bindingSets = candidateBindingSets.filter (set) -> node.is(set.selector)
|
|
bindingSets.sort (a, b) ->
|
|
if b.specificity == a.specificity
|
|
b.index - a.index
|
|
else
|
|
b.specificity - a.specificity
|
|
|
|
triggerCommandEvent: (keyEvent, commandName) ->
|
|
keyEvent.target = rootView[0] if keyEvent.target == document.body and window.rootView
|
|
commandEvent = $.Event(commandName)
|
|
commandEvent.keyEvent = keyEvent
|
|
aborted = false
|
|
commandEvent.abortKeyBinding = ->
|
|
@stopImmediatePropagation()
|
|
aborted = true
|
|
$(keyEvent.target).trigger(commandEvent)
|
|
aborted
|
|
|
|
multiKeystrokeStringForEvent: (event) ->
|
|
currentKeystroke = @keystrokeStringForEvent(event)
|
|
if @queuedKeystrokes
|
|
@queuedKeystrokes + ' ' + currentKeystroke
|
|
else
|
|
currentKeystroke
|
|
|
|
keystrokeStringForEvent: (event) ->
|
|
if event.originalEvent.keyIdentifier.indexOf('U+') == 0
|
|
hexCharCode = event.originalEvent.keyIdentifier[2..]
|
|
charCode = parseInt(hexCharCode, 16)
|
|
charCode = event.which if !@isAscii(charCode) and @isAscii(event.which)
|
|
key = @keyFromCharCode(charCode)
|
|
else
|
|
key = event.originalEvent.keyIdentifier.toLowerCase()
|
|
|
|
modifiers = []
|
|
if event.altKey and key not in Modifiers
|
|
modifiers.push 'alt'
|
|
if event.ctrlKey and key not in Modifiers
|
|
modifiers.push 'ctrl'
|
|
if event.metaKey and key not in Modifiers
|
|
modifiers.push 'meta'
|
|
|
|
if event.shiftKey and key not in Modifiers
|
|
isNamedKey = key.length > 1
|
|
modifiers.push 'shift' if isNamedKey
|
|
else
|
|
key = key.toLowerCase()
|
|
|
|
[modifiers..., key].join('-')
|
|
|
|
keystrokesByCommandForSelector: (selector)->
|
|
keystrokesByCommand = {}
|
|
for bindingSet in @bindingSets
|
|
for keystroke, command of bindingSet.commandsByKeystrokes
|
|
continue if selector? and selector != bindingSet.selector
|
|
keystrokesByCommand[command] ?= []
|
|
keystrokesByCommand[command].push keystroke
|
|
keystrokesByCommand
|
|
|
|
isAscii: (charCode) ->
|
|
0 <= charCode <= 127
|
|
|
|
keyFromCharCode: (charCode) ->
|
|
switch charCode
|
|
when 8 then 'backspace'
|
|
when 9 then 'tab'
|
|
when 13 then 'enter'
|
|
when 27 then 'escape'
|
|
when 32 then 'space'
|
|
when 127 then 'delete'
|
|
else String.fromCharCode(charCode)
|