Class: SublimeDSL::SublimeText::Keyboard

Inherits:
Object
  • Object
show all
Defined in:
lib/sublime_dsl/sublime_text/keyboard.rb

Overview

A keyboard.

Defined Under Namespace

Classes: CharStroke, DSLReader, Key, KeyStroke, NullStroke

Constant Summary collapse

SUBLIME_MODIFIERS =
%w(shift ctrl alt super)
SUBLIME_KEYS =
%w(
  escape f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12 f13 f14 f15 f16 f17 f18 f19 f20 sysreq pause
  up down right left
  insert home end pageup pagedown backspace delete
  tab enter context_menu
  keypad0 keypad1 keypad2 keypad3 keypad4 keypad5 keypad6 keypad7 keypad8 keypad9
  keypad_period keypad_divide keypad_multiply keypad_minus keypad_plus keypad_enter
  clear break
  browser_back browser_forward browser_refresh browser_stop
  browser_search browser_favorites browser_home

  ` 0 1 2 3 4 5 6 7 8 9 - = +
  a b c d e f g h i j k l m n o p q r s t u v w x y z
  [ ] \\ ; ' , . /
  space
)
SUBLIME_ALIAS_MAP =
{
  'forward_slash' => '/',
  'backquote' => '`',
  'equals' => '=',
  'plus' => '+',
  'minus' => '-'
}
SUBLIME_ALIAS_RE =
/^(#{SUBLIME_ALIAS_MAP.keys.join('|')})$/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name) ⇒ Keyboard



77
78
79
80
81
82
83
84
85
# File 'lib/sublime_dsl/sublime_text/keyboard.rb', line 77

def initialize(name)
  @name = name
  @os = nil
  @modifiers = []
  @keys = []
  @keystrokes_hash = {}
  # Vintage generic character
  add_keystroke CharStroke.new('<character>')
end

Instance Attribute Details

#keysObject (readonly)

Key instances



74
75
76
# File 'lib/sublime_dsl/sublime_text/keyboard.rb', line 74

def keys
  @keys
end

#keystrokes_hashObject (readonly)

normalized spec => KeyStroke or CharStroke instance



75
76
77
# File 'lib/sublime_dsl/sublime_text/keyboard.rb', line 75

def keystrokes_hash
  @keystrokes_hash
end

#modifiersObject (readonly)

Key instances



74
75
76
# File 'lib/sublime_dsl/sublime_text/keyboard.rb', line 74

def modifiers
  @modifiers
end

#nameObject (readonly)

Returns the value of attribute name.



73
74
75
# File 'lib/sublime_dsl/sublime_text/keyboard.rb', line 73

def name
  @name
end

#osObject

Returns the value of attribute os.



73
74
75
# File 'lib/sublime_dsl/sublime_text/keyboard.rb', line 73

def os
  @os
end

Class Method Details

.get(name, root) ⇒ Object



62
63
64
65
66
67
68
69
70
71
# File 'lib/sublime_dsl/sublime_text/keyboard.rb', line 62

def self.get(name, root)
  file = nil
  Dir.chdir(root) do
    files = Dir['**/*.keyboard.rb']
    file = SublimeText.order_config(files).last
    file or raise Error, "file '#{name}.keyboard.rb' not found"
    file = File.expand_path(file)
  end
  DSLReader.new(file)._keyboard
end

.sublimeObject

The standard Sublime Text keyboard.



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/sublime_dsl/sublime_text/keyboard.rb', line 43

def self.sublime
  @sublime ||= begin
    kb = Keyboard.new('Sublime Text')
    SUBLIME_MODIFIERS.each { |name| kb.add_modifier name }
    SUBLIME_KEYS.each { |name| kb.add_key name }
    # map_char and map_key call this method while @sublime is not yet set
    unless @defining_sublime
      @defining_sublime = true
      # FIXME: space => key_event nil, but generates a key_event when modified,
      # because it's a character
      # => put it in SUBLIME_ALIAS_MAP, but remember it was named 'space' when
      # generating its to_s ('ctrl+ ' will generate problems...)
      kb.map_char 'space' => ' ', key_event: nil
      @defining_sublime = false
    end
    kb
  end
end

Instance Method Details

#add_key(name) ⇒ Object

Create a Key name and adds the corresponding KeyStroke to this keyboard.

  • If name is one character long, the KeyStroke generates a chr_event name and no key_event.

  • Otherwise, the KeyStroke generates no chr_event, and a key_event name if name is a Sublime Text key name.



124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/sublime_dsl/sublime_text/keyboard.rb', line 124

def add_key(name)
  k = Key.new(name)
  k.st_name = name if SUBLIME_KEYS.include?(name)
  @keys << k
  ks = KeyStroke.new([], k)
  if name.length == 1
    ks.chr_event = name
  else
    ks.key_event = k.st_name
  end
  add_keystroke ks

  k
end

#add_keystroke(ks) ⇒ Object



175
176
177
# File 'lib/sublime_dsl/sublime_text/keyboard.rb', line 175

def add_keystroke(ks)
  @keystrokes_hash[ks.to_spec] = ks
end

#add_modifier(name) ⇒ Object



104
105
106
107
108
# File 'lib/sublime_dsl/sublime_text/keyboard.rb', line 104

def add_modifier(name)
  m = Key.new(name)
  m.st_name = name if SUBLIME_MODIFIERS.include?(name)
  @modifiers << m
end

#assign_default_key_event(keystroke) ⇒ Object

Assign the default key_event of a new keystroke (before its registration). It will be the modified key_event of a less specific keystroke.

For instance, if we register ‘shift+ctrl+keypad5’, and ‘shift+keypad5’ has key event ‘clear’, this assigns ‘ctrl+clear’. If there are several possibilities, the one(s) with the most modifiers are selected. If there are ex-aequo, the order of precedence is the order of registration of the modifiers.



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/sublime_dsl/sublime_text/keyboard.rb', line 285

def assign_default_key_event(keystroke)

  # the ST keyboard: just register
  if self == Keyboard.sublime
    spec = keystroke.to_spec
    keystroke.key_event = spec if spec.length > 1
    return
  end

  # we always have modifiers, as all non-modified keys are already registered
  keystroke.modifiers.empty? and raise Error, "bug: #{keystroke} is not registered"

  # if the key has a ST name, assume the ST equivalent
  if keystroke.key.st_name
    spec = keystroke.modifiers.map(&:st_name).join('+') << '+' << keystroke.key.st_name
    keystroke.key_event = spec
    return
  end

  # the candidates are the registered keystrokes for that key with all
  # modifiers included in the passed modifiers (so at least the keystroke
  # for the key itself)
  candidates = keystrokes_hash.values.select do |ks|
    ks.key == keystroke.key &&
      ks.modifiers - keystroke.modifiers == []
  end
  candidates.empty? and raise Error, "bug: nothing registered for #{keystroke}"

  if candidates.length > 1

    # select the one(s) with the most modifiers
    max = 0
    candidates.each do |ks|
      max = ks.modifiers.length if ks.modifiers.length > max
    end
    candidates.reject! { |ks| ks.modifiers.length < max }

    # apply modifier priority:
    # create the bit mask for each keystroke,
    # and then select the lowest one
    if candidates.length > 1
      sort_array =
        candidates.map do |ks|
          mask = 0
          ks.modifiers.each do |m|
            mask += (1 << modifiers_index_hash[m.name])
          end
          [ks, mask]
        end
      candidates = sort_array.sort_by(&:last).map(&:first)
    end

  end

  # select the reference keystroke
  ref = candidates.first
  if ref.key_event.nil?
    keystroke.key_event = nil
    return
  end

  # apply the modifier delta versus the reference
  delta = keystroke.modifiers - ref.modifiers
  spec = delta.map(&:st_name).join('+') << '+' << ref.key_event
  keystroke.key_event = Keyboard.sublime.ensure_keystroke(spec).to_spec

end

#ensure_keystroke(spec) ⇒ Object

Returns a KeyStroke or CharStroke for spec, and adds it to the registered keystrokes if not already there. Raises an exception if spec is not valid.

  • If spec is one character long, returns a KeyStroke or CharStroke if found, otherwise creates a new CharStroke.

  • If spec is more than one character long, the key has to exist, as well as the modifiers if any, otherwise an exception is raised. If the corresponding KeyStroke is not found, it will be created and registered.



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/sublime_dsl/sublime_text/keyboard.rb', line 217

def ensure_keystroke(spec)

  # split the specification
  #   ctrl++ -> ['ctrl', '+']
  #   ctrl+num+ -> ['ctrl', 'num+']
  *modifier_names, key_name = spec.split(/\+(?!$)/)

  # normalize the key name
  self == Keyboard.sublime and
    key_name.sub! SUBLIME_ALIAS_RE, SUBLIME_ALIAS_MAP

  # check & reorder the modifiers
  unless modifier_names.empty?
    sorted = []
    modifier_names.each do |name|
      i = modifiers_index_hash[name] or
        raise Error, "invalid modifier #{name.inspect} for keyboard #{self.name}"
      sorted[i] = name
    end
    modifier_names = sorted.compact
  end

  # if there is a registered keystroke for this spec, return it
  std_spec = [*modifier_names, key_name].join('+')
  ks = keystrokes_hash[std_spec]
  return ks if ks

  # shift + character is not ok
  modifiers = modifier_names.map { |n| modifier(n) }
  modifiers.map(&:st_name) == ['shift'] && key_name.length == 1 and
    raise Error, "#{spec.to_source(true)} is invalid: specify the corresponding character"

  key = key(key_name)

  # The ST keyboard accepts any character as a valid key
  if self == Keyboard.sublime && key.nil?
    key_name.length == 1 or raise Error, "invalid key name in #{spec.to_source(true)}"
    key = add_key(key_name)
    return keystrokes_hash[key_name] if modifiers.empty?
  end

  if key
    # registered key
    ks = KeyStroke.new(modifiers, key)
    assign_default_key_event ks
  else
    # unregistered: has to be a character
    key_name.length == 1 or
      raise Error, "#{spec.inspect}: key #{key_name.inspect} is undefined"
    modifier_names.empty? or
      raise Error, "#{spec.inspect}: #{key_name.inspect} is not a key"
    ks = CharStroke.new(key_name)
  end

  add_keystroke ks

  ks
end

#key(name) ⇒ Object



100
101
102
# File 'lib/sublime_dsl/sublime_text/keyboard.rb', line 100

def key(name)
  keys.find { |k| k.name == name }
end

#keystroke_for_sublime_spec(st_spec) ⇒ Object



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/sublime_dsl/sublime_text/keyboard.rb', line 183

def keystroke_for_sublime_spec(st_spec)
  st_ks = Keyboard.sublime.ensure_keystroke(st_spec)
  st_spec = st_ks.to_spec   # standardize

  # return the first one with a key event = the passed spec
  this_ks = keystrokes.find { |ks| ks.key_event == st_spec }
  return this_ks if this_ks

  # if one char, no problem
  return ensure_keystroke(st_spec) if st_spec.length == 1 || st_spec == '<character>'

  # not (yet?) registered: find a keystroke with the same key
  base_ks = keystrokes.find { |ks| ks.key && ks.key.st_name == st_ks.key.name }
  if base_ks
    this_spec = st_ks.modifiers.map(&:name).join('+') << '+' << base_ks.key.name
    this_ks = ensure_keystroke(this_spec)
    return this_ks
  end

  NullStroke.new(st_spec)
end

#keystrokesObject



179
180
181
# File 'lib/sublime_dsl/sublime_text/keyboard.rb', line 179

def keystrokes
  keystrokes_hash.values
end

#map_char(options = {}) ⇒ Object

Map a keystroke to a chr_event. Optionally sets the key_event.



161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/sublime_dsl/sublime_text/keyboard.rb', line 161

def map_char(options = {})
  ks_name = options.keys.first
  ks = ensure_keystroke(ks_name)
  ks.chr_event = options[ks_name]
  if options.has_key?(:key_event)
    st_spec = options[:key_event]
    if st_spec
      st_ks = Keyboard.sublime.ensure_keystroke(st_spec)
      st_spec = st_ks.to_spec
    end
    ks.key_event = st_spec
  end
end

#map_key(spec, st_spec) ⇒ Object

Assigns the key_event for the keystroke spec. st_spec must be a valid ST keystroke, or nil if the keystroke is not seen by ST.



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/sublime_dsl/sublime_text/keyboard.rb', line 143

def map_key(spec, st_spec)
  ks = ensure_keystroke(spec)
  if st_spec.nil?
    ks.key_event = nil
  else
    st_ks = Keyboard.sublime.ensure_keystroke(st_spec)
    st_spec = st_ks.to_spec
    if ks.modifiers.empty?
      ks.key.st_name = st_spec
      ks.key_event = st_spec if st_spec.length > 1
    else
      ks.key_event = st_spec
    end
  end
end

#map_modifier(name, st_name) ⇒ Object



110
111
112
113
114
# File 'lib/sublime_dsl/sublime_text/keyboard.rb', line 110

def map_modifier(name, st_name)
  m = modifier(name) or raise Error, "unknown modifier: '#{name}'"
  SUBLIME_MODIFIERS.include?(st_name) or raise Error, "invalid ST modifier: #{st_name}"
  m.st_name = st_name
end

#modifier(name) ⇒ Object



96
97
98
# File 'lib/sublime_dsl/sublime_text/keyboard.rb', line 96

def modifier(name)
  modifiers.find { |k| k.name == name }
end

#modifiers_index_hashObject



353
354
355
356
357
358
359
# File 'lib/sublime_dsl/sublime_text/keyboard.rb', line 353

def modifiers_index_hash
  @modifiers_index_hash ||= begin
    h = {}
    modifiers.each_with_index { |m, i| h[m.name] = i }
    h
  end
end