chromeos: bluetooth: add BluetoothNodeClient
[chromium-blink-merge.git] / tools / gen_keyboard_overlay_data / gen_keyboard_overlay_data.py
blob62d67d7ed2ddf4b10182e1c69a1f51ea1bdcd62a
1 #!/usr/bin/env python
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """Generate keyboard layout and hotkey data for the keyboard overlay.
8 This script fetches data from the keyboard layout and hotkey data spreadsheet,
9 and output the data depending on the option.
11 --cc: Rewrites a part of C++ code in
12 chrome/browser/chromeos/webui/keyboard_overlay_ui.cc
14 --grd: Rewrites a part of grd messages in
15 chrome/app/generated_resources.grd
17 --js: Rewrites the entire JavaScript code in
18 chrome/browser/resources/keyboard_overlay/keyboard_overlay_data.js
20 --altgr: Rewrites a list of layouts in
21 chrome/browser/chromeos/input_method/xkeyboard.cc
23 These options can be specified at the same time.
25 e.g.
26 python gen_keyboard_overlay_data.py --cc --grd --js
28 The output directory of the generated files can be changed with --outdir.
30 e.g. (This will generate tmp/xkeyboard.cc)
31 python gen_keyboard_overlay_data.py --outdir=tmp --altgr
32 """
34 import cStringIO
35 import datetime
36 import gdata.spreadsheet.service
37 import getpass
38 import json
39 import optparse
40 import os
41 import re
42 import sys
44 MODIFIER_SHIFT = 1 << 0
45 MODIFIER_CTRL = 1 << 1
46 MODIFIER_ALT = 1 << 2
48 KEYBOARD_GLYPH_SPREADSHEET_KEY = '0Ao3KldW9piwEdExLbGR6TmZ2RU9aUjFCMmVxWkVqVmc'
49 HOTKEY_SPREADSHEET_KEY = '0AqzoqbAMLyEPdE1RQXdodk1qVkFyTWtQbUxROVM1cXc'
50 CC_OUTDIR = 'chrome/browser/ui/webui/chromeos'
51 CC_FILENAME = 'keyboard_overlay_ui.cc'
52 GRD_OUTDIR = 'chrome/app'
53 GRD_FILENAME = 'generated_resources.grd'
54 JS_OUTDIR = 'chrome/browser/resources/chromeos'
55 JS_FILENAME = 'keyboard_overlay_data.js'
56 ALTGR_OUTDIR = 'chrome/browser/chromeos/input_method'
57 ALTGR_FILENAME = 'xkeyboard_data.h'
58 CC_START = r'IDS_KEYBOARD_OVERLAY_INSTRUCTIONS_HIDE },'
59 CC_END = r'};'
60 GRD_START = """Escape to hide
61 </message>"""
62 GRD_END = r' </if>'
64 LABEL_MAP = {
65 'glyph_arrow_down': 'down',
66 'glyph_arrow_left': 'left',
67 'glyph_arrow_right': 'right',
68 'glyph_arrow_up': 'up',
69 'glyph_back': 'back',
70 'glyph_backspace': 'backspace',
71 'glyph_brightness_down': 'bright down',
72 'glyph_brightness_up': 'bright up',
73 'glyph_enter': 'enter',
74 'glyph_forward': 'forward',
75 'glyph_fullscreen': 'full screen',
76 # Kana/Eisu key on Japanese keyboard
77 'glyph_ime': u'\u304b\u306a\u0020\u002f\u0020\u82f1\u6570',
78 'glyph_lock': 'lock',
79 'glyph_overview': 'next window',
80 'glyph_power': 'power',
81 'glyph_right': 'right',
82 'glyph_reload': 'reload',
83 'glyph_search': 'search',
84 'glyph_shift': 'shift',
85 'glyph_tab': 'tab',
86 'glyph_tools': 'tools',
87 'glyph_volume_down': 'vol. down',
88 'glyph_volume_mute': 'mute',
89 'glyph_volume_up': 'vol. up',
92 INPUT_METHOD_ID_TO_OVERLAY_ID = {
93 'm17n:ar:kbd': 'ar',
94 'm17n:fa:isiri': 'ar',
95 'm17n:hi:itrans': 'hi',
96 'm17n:th:kesmanee': 'th',
97 'm17n:th:pattachote': 'th',
98 'm17n:th:tis820': 'th',
99 'm17n:vi:tcvn': 'vi',
100 'm17n:vi:telex': 'vi',
101 'm17n:vi:viqr': 'vi',
102 'm17n:vi:vni': 'vi',
103 'm17n:zh:cangjie': 'zh_TW',
104 'm17n:zh:quick': 'zh_TW',
105 'mozc': 'en_US',
106 'mozc-chewing': 'zh_TW',
107 'mozc-dv': 'en_US_dvorak',
108 'mozc-hangul': 'ko',
109 'mozc-jp': 'ja',
110 'pinyin': 'zh_CN',
111 'pinyin-dv': 'en_US_dvorak',
112 'xkb:be::fra': 'fr',
113 'xkb:be::ger': 'de',
114 'xkb:be::nld': 'nl',
115 'xkb:bg::bul': 'bg',
116 'xkb:bg:phonetic:bul': 'bg',
117 'xkb:br::por': 'pt_BR',
118 'xkb:ca::fra': 'fr_CA',
119 'xkb:ca:eng:eng': 'ca',
120 'xkb:ch::ger': 'de',
121 'xkb:ch:fr:fra': 'fr',
122 'xkb:cz::cze': 'cs',
123 'xkb:de::ger': 'de',
124 'xkb:de:neo:ger': 'de_neo',
125 'xkb:dk::dan': 'da',
126 'xkb:ee::est': 'et',
127 'xkb:es::spa': 'es',
128 'xkb:es:cat:cat': 'ca',
129 'xkb:fi::fin': 'fi',
130 'xkb:fr::fra': 'fr',
131 'xkb:gb:dvorak:eng': 'en_GB_dvorak',
132 'xkb:gb:extd:eng': 'en_GB',
133 'xkb:gr::gre': 'el',
134 'xkb:hr::scr': 'hr',
135 'xkb:hu::hun': 'hu',
136 'xkb:il::heb': 'iw',
137 'xkb:it::ita': 'it',
138 'xkb:jp::jpn': 'ja',
139 'xkb:kr:kr104:kor': 'ko',
140 'xkb:latam::spa': 'es_419',
141 'xkb:lt::lit': 'lt',
142 'xkb:lv:apostrophe:lav': 'lv',
143 'xkb:no::nob': 'no',
144 'xkb:pl::pol': 'pl',
145 'xkb:pt::por': 'pt_PT',
146 'xkb:ro::rum': 'ro',
147 'xkb:rs::srp': 'sr',
148 'xkb:ru::rus': 'ru',
149 'xkb:ru:phonetic:rus': 'ru',
150 'xkb:se::swe': 'sv',
151 'xkb:si::slv': 'sl',
152 'xkb:sk::slo': 'sk',
153 'xkb:tr::tur': 'tr',
154 'xkb:ua::ukr': 'uk',
155 'xkb:us::eng': 'en_US',
156 'xkb:us:altgr-intl:eng': 'en_US_altgr_intl',
157 'xkb:us:colemak:eng': 'en_US_colemak',
158 'xkb:us:dvorak:eng': 'en_US_dvorak',
159 'xkb:us:intl:eng': 'en_US_intl',
160 'zinnia-japanese': 'ja',
163 COPYRIGHT_HEADER_TEMPLATE=(
164 """// Copyright (c) %s The Chromium Authors. All rights reserved.
165 // Use of this source code is governed by a BSD-style license that can be
166 // found in the LICENSE file.
167 """)
169 # A snippet for grd file
170 GRD_SNIPPET_TEMPLATE=""" <message name="%s" desc="%s">
172 </message>
175 # A snippet for C++ file
176 CC_SNIPPET_TEMPLATE=""" { "%s", %s },
179 ALTGR_TEMPLATE=(
180 """// This file was generated by 'gen_keyboard_overlay_data.py --altgr'
182 #ifndef CHROME_BROWSER_CHROMEOS_INPUT_METHOD_XKEYBOARD_DATA_H_
183 #define CHROME_BROWSER_CHROMEOS_INPUT_METHOD_XKEYBOARD_DATA_H_
185 namespace chromeos {
186 namespace input_method {
188 // These are the input method IDs that shouldn't remap the right alt key.
189 const char* kKeepRightAltInputMethods[] = {
193 // These are the overlay names with caps lock remapped
194 const char* kCapsLockRemapped[] = {
198 } // input_method
199 } // chromeos
201 #endif // CHROME_BROWSER_CHROMEOS_INPUT_METHOD_XKEYBOARD_DATA_H_
202 """)
204 def SplitBehavior(behavior):
205 """Splits the behavior to compose a message or i18n-content value.
207 Examples:
208 'Activate last tab' => ['Activate', 'last', 'tab']
209 'Close tab' => ['Close', 'tab']
211 return [x for x in re.split('[ ()"-.,]', behavior) if len(x) > 0]
214 def ToMessageName(behavior):
215 """Composes a message name for grd file.
217 Examples:
218 'Activate last tab' => IDS_KEYBOARD_OVERLAY_ACTIVATE_LAST_TAB
219 'Close tab' => IDS_KEYBOARD_OVERLAY_CLOSE_TAB
221 segments = [segment.upper() for segment in SplitBehavior(behavior)]
222 return 'IDS_KEYBOARD_OVERLAY_' + ('_'.join(segments))
225 def ToMessageDesc(description):
226 """Composes a message description for grd file."""
227 message_desc = 'The text in the keyboard overlay to explain the shortcut'
228 if description:
229 message_desc = '%s (%s).' % (message_desc, description)
230 else:
231 message_desc += '.'
232 return message_desc
235 def Toi18nContent(behavior):
236 """Composes a i18n-content value for HTML/JavaScript files.
238 Examples:
239 'Activate last tab' => keyboardOverlayActivateLastTab
240 'Close tab' => keyboardOverlayCloseTab
242 segments = [segment.lower() for segment in SplitBehavior(behavior)]
243 result = 'keyboardOverlay'
244 for segment in segments:
245 result += segment[0].upper() + segment[1:]
246 return result
249 def ToKeys(hotkey):
250 """Converts the action value to shortcut keys used from JavaScript.
252 Examples:
253 'Ctrl - 9' => '9<>CTRL'
254 'Ctrl - Shift - Tab' => 'tab<>CTRL<>SHIFT'
256 values = hotkey.split(' - ')
257 modifiers = sorted(value.upper() for value in values
258 if value in ['Shift', 'Ctrl', 'Alt'])
259 keycode = [value.lower() for value in values
260 if value not in ['Shift', 'Ctrl', 'Alt']]
261 # The keys which are highlighted even without modifier keys.
262 base_keys = ['backspace', 'power']
263 if not modifiers and (keycode and keycode[0] not in base_keys):
264 return None
265 return '<>'.join(keycode + modifiers)
268 def ParseOptions():
269 """Parses the input arguemnts and returns options."""
270 # default_username = os.getusername() + '@google.com';
271 default_username = '%s@google.com' % os.environ.get('USER')
272 parser = optparse.OptionParser()
273 parser.add_option('--key', dest='key',
274 help='The key of the spreadsheet (required).')
275 parser.add_option('--username', dest='username',
276 default=default_username,
277 help='Your user name (default: %s).' % default_username)
278 parser.add_option('--password', dest='password',
279 help='Your password.')
280 parser.add_option('--account_type', default='GOOGLE', dest='account_type',
281 help='Account type used for gdata login (default: GOOGLE)')
282 parser.add_option('--js', dest='js', default=False, action='store_true',
283 help='Output js file.')
284 parser.add_option('--grd', dest='grd', default=False, action='store_true',
285 help='Output resource file.')
286 parser.add_option('--cc', dest='cc', default=False, action='store_true',
287 help='Output cc file.')
288 parser.add_option('--altgr', dest='altgr', default=False, action='store_true',
289 help='Output altgr file.')
290 parser.add_option('--outdir', dest='outdir', default=None,
291 help='Specify the directory files are generated.')
292 (options, unused_args) = parser.parse_args()
294 if not options.username.endswith('google.com'):
295 print 'google.com account is necessary to use this script.'
296 sys.exit(-1)
298 if (not (options.js or options.grd or options.cc or options.altgr)):
299 print 'Either --js, --grd, --cc or --altgr needs to be specified.'
300 sys.exit(-1)
302 # Get the password from the terminal, if needed.
303 if not options.password:
304 options.password = getpass.getpass(
305 'Application specific password for %s: ' % options.username)
306 return options
309 def InitClient(options):
310 """Initializes the spreadsheet client."""
311 client = gdata.spreadsheet.service.SpreadsheetsService()
312 client.email = options.username
313 client.password = options.password
314 client.source = 'Spread Sheet'
315 client.account_type = options.account_type
316 print 'Logging in as %s (%s)' % (client.email, client.account_type)
317 client.ProgrammaticLogin()
318 return client
321 def PrintDiffs(message, lhs, rhs):
322 """Prints the differences between |lhs| and |rhs|."""
323 dif = set(lhs).difference(rhs)
324 if dif:
325 print message, ', '.join(dif)
328 def FetchSpreadsheetFeeds(client, key, sheets, cols):
329 """Fetch feeds from the spreadsheet.
331 Args:
332 client: A spreadsheet client to be used for fetching data.
333 key: A key string of the spreadsheet to be fetched.
334 sheets: A list of the sheet names to read data from.
335 cols: A list of columns to read data from.
337 worksheets_feed = client.GetWorksheetsFeed(key)
338 print 'Fetching data from the worksheet: %s' % worksheets_feed.title.text
339 worksheets_data = {}
340 titles = []
341 for entry in worksheets_feed.entry:
342 worksheet_id = entry.id.text.split('/')[-1]
343 list_feed = client.GetListFeed(key, worksheet_id)
344 list_data = []
345 # Hack to deal with sheet names like 'sv (Copy of fl)'
346 title = list_feed.title.text.split('(')[0].strip()
347 titles.append(title)
348 if title not in sheets:
349 continue
350 print 'Reading data from the sheet: %s' % list_feed.title.text
351 for i, entry in enumerate(list_feed.entry):
352 line_data = {}
353 for k in entry.custom:
354 if (k not in cols) or (not entry.custom[k].text):
355 continue
356 line_data[k] = entry.custom[k].text
357 list_data.append(line_data)
358 worksheets_data[title] = list_data
359 PrintDiffs('Exist only on the spreadsheet: ', titles, sheets)
360 PrintDiffs('Specified but do not exist on the spreadsheet: ', sheets, titles)
361 return worksheets_data
364 def FetchKeyboardGlyphData(client):
365 """Fetches the keyboard glyph data from the spreadsheet."""
366 glyph_cols = ['scancode', 'p0', 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7',
367 'p8', 'p9', 'label', 'format', 'notes']
368 keyboard_glyph_data = FetchSpreadsheetFeeds(
369 client, KEYBOARD_GLYPH_SPREADSHEET_KEY,
370 INPUT_METHOD_ID_TO_OVERLAY_ID.values(), glyph_cols)
371 ret = {}
372 for lang in keyboard_glyph_data:
373 ret[lang] = {}
374 keys = {}
375 for line in keyboard_glyph_data[lang]:
376 scancode = line.get('scancode')
377 if (not scancode) and line.get('notes'):
378 ret[lang]['layoutName'] = line['notes']
379 continue
380 del line['scancode']
381 if 'notes' in line:
382 del line['notes']
383 if 'label' in line:
384 line['label'] = LABEL_MAP.get(line['label'], line['label'])
385 keys[scancode] = line
386 # Add a label to space key
387 if '39' not in keys:
388 keys['39'] = {'label': 'space'}
389 ret[lang]['keys'] = keys
390 return ret
393 def FetchLayoutsData(client):
394 """Fetches the keyboard glyph data from the spreadsheet."""
395 layout_names = ['U_layout', 'J_layout', 'E_layout', 'B_layout']
396 cols = ['scancode', 'x', 'y', 'w', 'h']
397 layouts = FetchSpreadsheetFeeds(client, KEYBOARD_GLYPH_SPREADSHEET_KEY,
398 layout_names, cols)
399 ret = {}
400 for layout_name, layout in layouts.items():
401 ret[layout_name[0]] = []
402 for row in layout:
403 line = []
404 for col in cols:
405 value = row.get(col)
406 if not value:
407 line.append('')
408 else:
409 if col != 'scancode':
410 value = float(value)
411 line.append(value)
412 ret[layout_name[0]].append(line)
413 return ret
416 def FetchHotkeyData(client):
417 """Fetches the hotkey data from the spreadsheet."""
418 hotkey_sheet = ['Cross Platform Behaviors']
419 hotkey_cols = ['behavior', 'context', 'kind', 'actionctrlctrlcmdonmac',
420 'chromeos', 'descriptionfortranslation']
421 hotkey_data = FetchSpreadsheetFeeds(client, HOTKEY_SPREADSHEET_KEY,
422 hotkey_sheet, hotkey_cols)
423 action_to_id = {}
424 id_to_behavior = {}
425 # (behavior, action)
426 result = []
427 for line in hotkey_data['Cross Platform Behaviors']:
428 if (not line.get('chromeos')) or (line.get('kind') != 'Key'):
429 continue
430 action = ToKeys(line['actionctrlctrlcmdonmac'])
431 if not action:
432 continue
433 behavior = line['behavior'].strip()
434 description = line.get('descriptionfortranslation')
435 result.append((behavior, action, description))
436 return result
439 def GenerateCopyrightHeader():
440 """Generates the copyright header for JavaScript code."""
441 return COPYRIGHT_HEADER_TEMPLATE % datetime.date.today().year
444 def UniqueBehaviors(hotkey_data):
445 """Retrieves a sorted list of unique behaviors from |hotkey_data|."""
446 return sorted(set((behavior, description) for (behavior, _, description)
447 in hotkey_data),
448 cmp=lambda x, y: cmp(ToMessageName(x[0]), ToMessageName(y[0])))
451 def GetPath(path_from_src):
452 """Returns the absolute path of the specified path."""
453 path = os.path.join(os.path.dirname(__file__), '../..', path_from_src)
454 if not os.path.isfile(path):
455 print 'WARNING: %s does not exist. Maybe moved or renamed?' % path
456 return path
459 def OutputFile(outpath, snippet):
460 """Output the snippet into the specified path."""
461 out = file(outpath, 'w')
462 out.write(GenerateCopyrightHeader() + '\n')
463 out.write(snippet)
464 print 'Output ' + os.path.normpath(outpath)
467 def RewriteFile(start, end, original_dir, original_filename, snippet,
468 outdir=None):
469 """Replaces a part of the specified file with snippet and outputs it."""
470 original_path = GetPath(os.path.join(original_dir, original_filename))
471 original = file(original_path, 'r')
472 original_content = original.read()
473 original.close()
474 if outdir:
475 outpath = os.path.join(outdir, original_filename)
476 else:
477 outpath = original_path
478 out = file(outpath, 'w')
479 rx = re.compile(r'%s\n.*?%s\n' % (re.escape(start), re.escape(end)),
480 re.DOTALL)
481 new_content = re.sub(rx, '%s\n%s%s\n' % (start, snippet, end),
482 original_content)
483 out.write(new_content)
484 out.close()
485 print 'Output ' + os.path.normpath(outpath)
488 def OutputJson(keyboard_glyph_data, hotkey_data, layouts, var_name, outdir):
489 """Outputs the keyboard overlay data as a JSON file."""
490 action_to_id = {}
491 for (behavior, action, _) in hotkey_data:
492 i18nContent = Toi18nContent(behavior)
493 action_to_id[action] = i18nContent
494 data = {'keyboardGlyph': keyboard_glyph_data,
495 'shortcut': action_to_id,
496 'layouts': layouts,
497 'inputMethodIdToOverlayId': INPUT_METHOD_ID_TO_OVERLAY_ID}
499 if not outdir:
500 outdir = JS_OUTDIR
501 outpath = GetPath(os.path.join(outdir, JS_FILENAME))
502 json_data = json.dumps(data, sort_keys=True, indent=2)
503 # Remove redundant spaces after ','
504 json_data = json_data.replace(', \n', ',\n')
505 snippet = 'var %s = %s;\n' % (var_name, json_data)
506 OutputFile(outpath, snippet)
509 def OutputGrd(hotkey_data, outdir):
510 """Outputs a part of messages in the grd file."""
511 snippet = cStringIO.StringIO()
512 for (behavior, description) in UniqueBehaviors(hotkey_data):
513 snippet.write(GRD_SNIPPET_TEMPLATE %
514 (ToMessageName(behavior), ToMessageDesc(description),
515 behavior))
517 RewriteFile(GRD_START, GRD_END, GRD_OUTDIR, GRD_FILENAME, snippet.getvalue(),
518 outdir)
521 def OutputCC(hotkey_data, outdir):
522 """Outputs a part of code in the C++ file."""
523 snippet = cStringIO.StringIO()
524 for (behavior, _) in UniqueBehaviors(hotkey_data):
525 message_name = ToMessageName(behavior)
526 output = CC_SNIPPET_TEMPLATE % (Toi18nContent(behavior), message_name)
527 # Break the line if the line is longer than 80 characters
528 if len(output) > 80:
529 output = output.replace(' ' + message_name, '\n %s' % message_name)
530 snippet.write(output)
532 RewriteFile(CC_START, CC_END, CC_OUTDIR, CC_FILENAME, snippet.getvalue(),
533 outdir)
536 def OutputAltGr(keyboard_glyph_data, outdir):
537 """Outputs the keyboard overlay data as a JSON file."""
538 altgr_output = []
539 caps_lock_output = []
541 for input_method_id, layout in INPUT_METHOD_ID_TO_OVERLAY_ID.iteritems():
542 try:
543 # If left and right alt have different values, this layout to the list of
544 # layouts that don't remap the right alt key.
545 right_alt = keyboard_glyph_data[layout]["keys"]["E0 38"]["label"].strip()
546 left_alt = keyboard_glyph_data[layout]["keys"]["38"]["label"].strip()
547 if right_alt.lower() != left_alt.lower():
548 altgr_output.append(' "%s",' % input_method_id)
549 except KeyError:
550 pass
552 try:
553 caps_lock = keyboard_glyph_data[layout]["keys"]["E0 5B"]["label"].strip()
554 if caps_lock.lower() != "search":
555 caps_lock_output.append(' "%s",' % input_method_id)
556 except KeyError:
557 pass
559 if not outdir:
560 outdir = ALTGR_OUTDIR
561 outpath = GetPath(os.path.join(outdir, ALTGR_FILENAME))
562 snippet = ALTGR_TEMPLATE % ("\n".join(sorted(altgr_output)),
563 "\n".join(sorted(caps_lock_output)))
564 OutputFile(outpath, snippet)
567 def main():
568 options = ParseOptions()
569 client = InitClient(options)
570 hotkey_data = FetchHotkeyData(client)
572 if options.js or options.altgr:
573 keyboard_glyph_data = FetchKeyboardGlyphData(client)
575 if options.js:
576 layouts = FetchLayoutsData(client)
577 OutputJson(keyboard_glyph_data, hotkey_data, layouts, 'keyboardOverlayData',
578 options.outdir)
579 if options.grd:
580 OutputGrd(hotkey_data, options.outdir)
581 if options.cc:
582 OutputCC(hotkey_data, options.outdir)
583 if options.altgr:
584 OutputAltGr(keyboard_glyph_data, options.outdir)
587 if __name__ == '__main__':
588 main()