Separate Simple Backend creation from initialization.
[chromium-blink-merge.git] / tools / gen_keyboard_overlay_data / gen_keyboard_overlay_data.py
blobf8d3ba12d9ad50a087b102a3599a6935e080109a
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 These options can be specified at the same time.
22 e.g.
23 python gen_keyboard_overlay_data.py --cc --grd --js
25 The output directory of the generated files can be changed with --outdir.
27 e.g. (This will generate tmp/keyboard_overlay.js)
28 python gen_keyboard_overlay_data.py --outdir=tmp --js
29 """
31 import cStringIO
32 import datetime
33 import gdata.spreadsheet.service
34 import getpass
35 import json
36 import optparse
37 import os
38 import re
39 import sys
41 MODIFIER_SHIFT = 1 << 0
42 MODIFIER_CTRL = 1 << 1
43 MODIFIER_ALT = 1 << 2
45 KEYBOARD_GLYPH_SPREADSHEET_KEY = '0Ao3KldW9piwEdExLbGR6TmZ2RU9aUjFCMmVxWkVqVmc'
46 HOTKEY_SPREADSHEET_KEY = '0AqzoqbAMLyEPdE1RQXdodk1qVkFyTWtQbUxROVM1cXc'
47 CC_OUTDIR = 'chrome/browser/ui/webui/chromeos'
48 CC_FILENAME = 'keyboard_overlay_ui.cc'
49 GRD_OUTDIR = 'chrome/app'
50 GRD_FILENAME = 'chromeos_strings.grdp'
51 JS_OUTDIR = 'chrome/browser/resources/chromeos'
52 JS_FILENAME = 'keyboard_overlay_data.js'
53 CC_START = r'IDS_KEYBOARD_OVERLAY_INSTRUCTIONS_HIDE },'
54 CC_END = r'};'
55 GRD_START = r' <!-- BEGIN GENERATED KEYBOARD OVERLAY STRINGS -->'
56 GRD_END = r' <!-- END GENERATED KEYBOARD OVERLAY STRINGS -->'
58 LABEL_MAP = {
59 'glyph_arrow_down': 'down',
60 'glyph_arrow_left': 'left',
61 'glyph_arrow_right': 'right',
62 'glyph_arrow_up': 'up',
63 'glyph_back': 'back',
64 'glyph_backspace': 'backspace',
65 'glyph_brightness_down': 'bright down',
66 'glyph_brightness_up': 'bright up',
67 'glyph_enter': 'enter',
68 'glyph_forward': 'forward',
69 'glyph_fullscreen': 'maximize',
70 # Kana/Eisu key on Japanese keyboard
71 'glyph_ime': u'\u304b\u306a\u0020\u002f\u0020\u82f1\u6570',
72 'glyph_lock': 'lock',
73 'glyph_overview': 'switch window',
74 'glyph_power': 'power',
75 'glyph_right': 'right',
76 'glyph_reload': 'reload',
77 'glyph_search': 'search',
78 'glyph_shift': 'shift',
79 'glyph_tab': 'tab',
80 'glyph_tools': 'tools',
81 'glyph_volume_down': 'vol. down',
82 'glyph_volume_mute': 'mute',
83 'glyph_volume_up': 'vol. up',
86 INPUT_METHOD_ID_TO_OVERLAY_ID = {
87 'm17n:ar:kbd': 'ar',
88 'm17n:fa:isiri': 'ar',
89 'm17n:hi:itrans': 'hi',
90 'm17n:th:kesmanee': 'th',
91 'm17n:th:pattachote': 'th',
92 'm17n:th:tis820': 'th',
93 'm17n:vi:tcvn': 'vi',
94 'm17n:vi:telex': 'vi',
95 'm17n:vi:viqr': 'vi',
96 'm17n:vi:vni': 'vi',
97 'm17n:zh:cangjie': 'zh_TW',
98 'm17n:zh:quick': 'zh_TW',
99 'mozc': 'en_US',
100 'mozc-chewing': 'zh_TW',
101 'mozc-dv': 'en_US_dvorak',
102 'mozc-hangul': 'ko',
103 'mozc-jp': 'ja',
104 'pinyin': 'zh_CN',
105 'pinyin-dv': 'en_US_dvorak',
106 'xkb:be::fra': 'fr',
107 'xkb:be::ger': 'de',
108 'xkb:be::nld': 'nl',
109 'xkb:bg::bul': 'bg',
110 'xkb:bg:phonetic:bul': 'bg',
111 'xkb:br::por': 'pt_BR',
112 'xkb:ca::fra': 'fr_CA',
113 'xkb:ca:eng:eng': 'ca',
114 'xkb:ch::ger': 'de',
115 'xkb:ch:fr:fra': 'fr',
116 'xkb:cz::cze': 'cs',
117 'xkb:de::ger': 'de',
118 'xkb:de:neo:ger': 'de_neo',
119 'xkb:dk::dan': 'da',
120 'xkb:ee::est': 'et',
121 'xkb:es::spa': 'es',
122 'xkb:es:cat:cat': 'ca',
123 'xkb:fi::fin': 'fi',
124 'xkb:fr::fra': 'fr',
125 'xkb:gb:dvorak:eng': 'en_GB_dvorak',
126 'xkb:gb:extd:eng': 'en_GB',
127 'xkb:gr::gre': 'el',
128 'xkb:hr::scr': 'hr',
129 'xkb:hu::hun': 'hu',
130 'xkb:il::heb': 'iw',
131 'xkb:it::ita': 'it',
132 'xkb:jp::jpn': 'ja',
133 'xkb:kr:kr104:kor': 'ko',
134 'xkb:latam::spa': 'es_419',
135 'xkb:lt::lit': 'lt',
136 'xkb:lv:apostrophe:lav': 'lv',
137 'xkb:no::nob': 'no',
138 'xkb:pl::pol': 'pl',
139 'xkb:pt::por': 'pt_PT',
140 'xkb:ro::rum': 'ro',
141 'xkb:rs::srp': 'sr',
142 'xkb:ru::rus': 'ru',
143 'xkb:ru:phonetic:rus': 'ru',
144 'xkb:se::swe': 'sv',
145 'xkb:si::slv': 'sl',
146 'xkb:sk::slo': 'sk',
147 'xkb:tr::tur': 'tr',
148 'xkb:ua::ukr': 'uk',
149 'xkb:us::eng': 'en_US',
150 'xkb:us:altgr-intl:eng': 'en_US_altgr_intl',
151 'xkb:us:colemak:eng': 'en_US_colemak',
152 'xkb:us:dvorak:eng': 'en_US_dvorak',
153 'xkb:us:intl:eng': 'en_US_intl',
154 'zinnia-japanese': 'ja',
157 # The file was first generated in 2012 and we have a policy of not updating
158 # copyright dates.
159 COPYRIGHT_HEADER=\
160 """// Copyright (c) 2012 The Chromium Authors. All rights reserved.
161 // Use of this source code is governed by a BSD-style license that can be
162 // found in the LICENSE file.
165 # A snippet for grd file
166 GRD_SNIPPET_TEMPLATE=""" <message name="%s" desc="%s">
168 </message>
171 # A snippet for C++ file
172 CC_SNIPPET_TEMPLATE=""" { "%s", %s },
176 def SplitBehavior(behavior):
177 """Splits the behavior to compose a message or i18n-content value.
179 Examples:
180 'Activate last tab' => ['Activate', 'last', 'tab']
181 'Close tab' => ['Close', 'tab']
183 return [x for x in re.split('[ ()"-.,]', behavior) if len(x) > 0]
186 def ToMessageName(behavior):
187 """Composes a message name for grd file.
189 Examples:
190 'Activate last tab' => IDS_KEYBOARD_OVERLAY_ACTIVATE_LAST_TAB
191 'Close tab' => IDS_KEYBOARD_OVERLAY_CLOSE_TAB
193 segments = [segment.upper() for segment in SplitBehavior(behavior)]
194 return 'IDS_KEYBOARD_OVERLAY_' + ('_'.join(segments))
197 def ToMessageDesc(description):
198 """Composes a message description for grd file."""
199 message_desc = 'The text in the keyboard overlay to explain the shortcut'
200 if description:
201 message_desc = '%s (%s).' % (message_desc, description)
202 else:
203 message_desc += '.'
204 return message_desc
207 def Toi18nContent(behavior):
208 """Composes a i18n-content value for HTML/JavaScript files.
210 Examples:
211 'Activate last tab' => keyboardOverlayActivateLastTab
212 'Close tab' => keyboardOverlayCloseTab
214 segments = [segment.lower() for segment in SplitBehavior(behavior)]
215 result = 'keyboardOverlay'
216 for segment in segments:
217 result += segment[0].upper() + segment[1:]
218 return result
221 def ToKeys(hotkey):
222 """Converts the action value to shortcut keys used from JavaScript.
224 Examples:
225 'Ctrl - 9' => '9<>CTRL'
226 'Ctrl - Shift - Tab' => 'tab<>CTRL<>SHIFT'
228 values = hotkey.split(' - ')
229 modifiers = sorted(value.upper() for value in values
230 if value in ['Shift', 'Ctrl', 'Alt', 'Search'])
231 keycode = [value.lower() for value in values
232 if value not in ['Shift', 'Ctrl', 'Alt', 'Search']]
233 # The keys which are highlighted even without modifier keys.
234 base_keys = ['backspace', 'power']
235 if not modifiers and (keycode and keycode[0] not in base_keys):
236 return None
237 return '<>'.join(keycode + modifiers)
240 def ParseOptions():
241 """Parses the input arguemnts and returns options."""
242 # default_username = os.getusername() + '@google.com';
243 default_username = '%s@google.com' % os.environ.get('USER')
244 parser = optparse.OptionParser()
245 parser.add_option('--key', dest='key',
246 help='The key of the spreadsheet (required).')
247 parser.add_option('--username', dest='username',
248 default=default_username,
249 help='Your user name (default: %s).' % default_username)
250 parser.add_option('--password', dest='password',
251 help='Your password.')
252 parser.add_option('--account_type', default='GOOGLE', dest='account_type',
253 help='Account type used for gdata login (default: GOOGLE)')
254 parser.add_option('--js', dest='js', default=False, action='store_true',
255 help='Output js file.')
256 parser.add_option('--grd', dest='grd', default=False, action='store_true',
257 help='Output resource file.')
258 parser.add_option('--cc', dest='cc', default=False, action='store_true',
259 help='Output cc file.')
260 parser.add_option('--outdir', dest='outdir', default=None,
261 help='Specify the directory files are generated.')
262 (options, unused_args) = parser.parse_args()
264 if not options.username.endswith('google.com'):
265 print 'google.com account is necessary to use this script.'
266 sys.exit(-1)
268 if (not (options.js or options.grd or options.cc)):
269 print 'Either --js, --grd, or --cc needs to be specified.'
270 sys.exit(-1)
272 # Get the password from the terminal, if needed.
273 if not options.password:
274 options.password = getpass.getpass(
275 'Application specific password for %s: ' % options.username)
276 return options
279 def InitClient(options):
280 """Initializes the spreadsheet client."""
281 client = gdata.spreadsheet.service.SpreadsheetsService()
282 client.email = options.username
283 client.password = options.password
284 client.source = 'Spread Sheet'
285 client.account_type = options.account_type
286 print 'Logging in as %s (%s)' % (client.email, client.account_type)
287 client.ProgrammaticLogin()
288 return client
291 def PrintDiffs(message, lhs, rhs):
292 """Prints the differences between |lhs| and |rhs|."""
293 dif = set(lhs).difference(rhs)
294 if dif:
295 print message, ', '.join(dif)
298 def FetchSpreadsheetFeeds(client, key, sheets, cols):
299 """Fetch feeds from the spreadsheet.
301 Args:
302 client: A spreadsheet client to be used for fetching data.
303 key: A key string of the spreadsheet to be fetched.
304 sheets: A list of the sheet names to read data from.
305 cols: A list of columns to read data from.
307 worksheets_feed = client.GetWorksheetsFeed(key)
308 print 'Fetching data from the worksheet: %s' % worksheets_feed.title.text
309 worksheets_data = {}
310 titles = []
311 for entry in worksheets_feed.entry:
312 worksheet_id = entry.id.text.split('/')[-1]
313 list_feed = client.GetListFeed(key, worksheet_id)
314 list_data = []
315 # Hack to deal with sheet names like 'sv (Copy of fl)'
316 title = list_feed.title.text.split('(')[0].strip()
317 titles.append(title)
318 if title not in sheets:
319 continue
320 print 'Reading data from the sheet: %s' % list_feed.title.text
321 for i, entry in enumerate(list_feed.entry):
322 line_data = {}
323 for k in entry.custom:
324 if (k not in cols) or (not entry.custom[k].text):
325 continue
326 line_data[k] = entry.custom[k].text
327 list_data.append(line_data)
328 worksheets_data[title] = list_data
329 PrintDiffs('Exist only on the spreadsheet: ', titles, sheets)
330 PrintDiffs('Specified but do not exist on the spreadsheet: ', sheets, titles)
331 return worksheets_data
334 def FetchKeyboardGlyphData(client):
335 """Fetches the keyboard glyph data from the spreadsheet."""
336 glyph_cols = ['scancode', 'p0', 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7',
337 'p8', 'p9', 'label', 'format', 'notes']
338 keyboard_glyph_data = FetchSpreadsheetFeeds(
339 client, KEYBOARD_GLYPH_SPREADSHEET_KEY,
340 INPUT_METHOD_ID_TO_OVERLAY_ID.values(), glyph_cols)
341 ret = {}
342 for lang in keyboard_glyph_data:
343 ret[lang] = {}
344 keys = {}
345 for line in keyboard_glyph_data[lang]:
346 scancode = line.get('scancode')
347 if (not scancode) and line.get('notes'):
348 ret[lang]['layoutName'] = line['notes']
349 continue
350 del line['scancode']
351 if 'notes' in line:
352 del line['notes']
353 if 'label' in line:
354 line['label'] = LABEL_MAP.get(line['label'], line['label'])
355 keys[scancode] = line
356 # Add a label to space key
357 if '39' not in keys:
358 keys['39'] = {'label': 'space'}
359 ret[lang]['keys'] = keys
360 return ret
363 def FetchLayoutsData(client):
364 """Fetches the keyboard glyph data from the spreadsheet."""
365 layout_names = ['U_layout', 'J_layout', 'E_layout', 'B_layout']
366 cols = ['scancode', 'x', 'y', 'w', 'h']
367 layouts = FetchSpreadsheetFeeds(client, KEYBOARD_GLYPH_SPREADSHEET_KEY,
368 layout_names, cols)
369 ret = {}
370 for layout_name, layout in layouts.items():
371 ret[layout_name[0]] = []
372 for row in layout:
373 line = []
374 for col in cols:
375 value = row.get(col)
376 if not value:
377 line.append('')
378 else:
379 if col != 'scancode':
380 value = float(value)
381 line.append(value)
382 ret[layout_name[0]].append(line)
383 return ret
386 def FetchHotkeyData(client):
387 """Fetches the hotkey data from the spreadsheet."""
388 hotkey_sheet = ['Cross Platform Behaviors']
389 hotkey_cols = ['behavior', 'context', 'kind', 'actionctrlctrlcmdonmac',
390 'chromeos', 'descriptionfortranslation']
391 hotkey_data = FetchSpreadsheetFeeds(client, HOTKEY_SPREADSHEET_KEY,
392 hotkey_sheet, hotkey_cols)
393 action_to_id = {}
394 id_to_behavior = {}
395 # (behavior, action)
396 result = []
397 for line in hotkey_data['Cross Platform Behaviors']:
398 if (not line.get('chromeos')) or (line.get('kind') != 'Key'):
399 continue
400 action = ToKeys(line['actionctrlctrlcmdonmac'])
401 if not action:
402 continue
403 behavior = line['behavior'].strip()
404 description = line.get('descriptionfortranslation')
405 result.append((behavior, action, description))
406 return result
409 def UniqueBehaviors(hotkey_data):
410 """Retrieves a sorted list of unique behaviors from |hotkey_data|."""
411 return sorted(set((behavior, description) for (behavior, _, description)
412 in hotkey_data),
413 cmp=lambda x, y: cmp(ToMessageName(x[0]), ToMessageName(y[0])))
416 def GetPath(path_from_src):
417 """Returns the absolute path of the specified path."""
418 path = os.path.join(os.path.dirname(__file__), '../..', path_from_src)
419 if not os.path.isfile(path):
420 print 'WARNING: %s does not exist. Maybe moved or renamed?' % path
421 return path
424 def OutputFile(outpath, snippet):
425 """Output the snippet into the specified path."""
426 out = file(outpath, 'w')
427 out.write(COPYRIGHT_HEADER + '\n')
428 out.write(snippet)
429 print 'Output ' + os.path.normpath(outpath)
432 def RewriteFile(start, end, original_dir, original_filename, snippet,
433 outdir=None):
434 """Replaces a part of the specified file with snippet and outputs it."""
435 original_path = GetPath(os.path.join(original_dir, original_filename))
436 original = file(original_path, 'r')
437 original_content = original.read()
438 original.close()
439 if outdir:
440 outpath = os.path.join(outdir, original_filename)
441 else:
442 outpath = original_path
443 out = file(outpath, 'w')
444 rx = re.compile(r'%s\n.*?%s\n' % (re.escape(start), re.escape(end)),
445 re.DOTALL)
446 new_content = re.sub(rx, '%s\n%s%s\n' % (start, snippet, end),
447 original_content)
448 out.write(new_content)
449 out.close()
450 print 'Output ' + os.path.normpath(outpath)
453 def OutputJson(keyboard_glyph_data, hotkey_data, layouts, var_name, outdir):
454 """Outputs the keyboard overlay data as a JSON file."""
455 action_to_id = {}
456 for (behavior, action, _) in hotkey_data:
457 i18nContent = Toi18nContent(behavior)
458 action_to_id[action] = i18nContent
459 data = {'keyboardGlyph': keyboard_glyph_data,
460 'shortcut': action_to_id,
461 'layouts': layouts,
462 'inputMethodIdToOverlayId': INPUT_METHOD_ID_TO_OVERLAY_ID}
464 if not outdir:
465 outdir = JS_OUTDIR
466 outpath = GetPath(os.path.join(outdir, JS_FILENAME))
467 json_data = json.dumps(data, sort_keys=True, indent=2)
468 # Remove redundant spaces after ','
469 json_data = json_data.replace(', \n', ',\n')
470 # Replace double quotes with single quotes to avoid lint warnings.
471 json_data = json_data.replace('\"', '\'')
472 snippet = 'var %s = %s;\n' % (var_name, json_data)
473 OutputFile(outpath, snippet)
476 def OutputGrd(hotkey_data, outdir):
477 """Outputs a part of messages in the grd file."""
478 snippet = cStringIO.StringIO()
479 for (behavior, description) in UniqueBehaviors(hotkey_data):
480 # Do not generate message for 'Show wrench menu'. It is handled manually
481 # based on branding.
482 if behavior == 'Show wrench menu':
483 continue
484 snippet.write(GRD_SNIPPET_TEMPLATE %
485 (ToMessageName(behavior), ToMessageDesc(description),
486 behavior))
488 RewriteFile(GRD_START, GRD_END, GRD_OUTDIR, GRD_FILENAME, snippet.getvalue(),
489 outdir)
492 def OutputCC(hotkey_data, outdir):
493 """Outputs a part of code in the C++ file."""
494 snippet = cStringIO.StringIO()
495 for (behavior, _) in UniqueBehaviors(hotkey_data):
496 message_name = ToMessageName(behavior)
497 output = CC_SNIPPET_TEMPLATE % (Toi18nContent(behavior), message_name)
498 # Break the line if the line is longer than 80 characters
499 if len(output) > 80:
500 output = output.replace(' ' + message_name, '\n %s' % message_name)
501 snippet.write(output)
503 RewriteFile(CC_START, CC_END, CC_OUTDIR, CC_FILENAME, snippet.getvalue(),
504 outdir)
507 def main():
508 options = ParseOptions()
509 client = InitClient(options)
510 hotkey_data = FetchHotkeyData(client)
512 if options.js:
513 keyboard_glyph_data = FetchKeyboardGlyphData(client)
515 if options.js:
516 layouts = FetchLayoutsData(client)
517 OutputJson(keyboard_glyph_data, hotkey_data, layouts, 'keyboardOverlayData',
518 options.outdir)
519 if options.grd:
520 OutputGrd(hotkey_data, options.outdir)
521 if options.cc:
522 OutputCC(hotkey_data, options.outdir)
525 if __name__ == '__main__':
526 main()