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. 5 6 """Generate keyboard layout and hotkey data for the keyboard overlay. 7 8 This script fetches data from the keyboard layout and hotkey data spreadsheet, 9 and output the data depending on the option. 10 11 --cc: Rewrites a part of C++ code in 12 chrome/browser/chromeos/webui/keyboard_overlay_ui.cc 13 14 --grd: Rewrites a part of grd messages in 15 chrome/app/generated_resources.grd 16 17 --js: Rewrites the entire JavaScript code in 18 chrome/browser/resources/keyboard_overlay/keyboard_overlay_data.js 19 20 These options can be specified at the same time. 21 22 e.g. 23 python gen_keyboard_overlay_data.py --cc --grd --js 24 25 The output directory of the generated files can be changed with --outdir. 26 27 e.g. (This will generate tmp/keyboard_overlay.js) 28 python gen_keyboard_overlay_data.py --outdir=tmp --js 29 """ 30 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 40 41 MODIFIER_SHIFT = 1 << 0 42 MODIFIER_CTRL = 1 << 1 43 MODIFIER_ALT = 1 << 2 44 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 -->' 57 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': 'full screen', 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', 84 }; 85 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', 155 } 156 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. 163 164 // This is a generated file but may contain local modifications. See 165 // src/tools/gen_keyboard_overlay_data/gen_keyboard_overlay_data.py --help 166 """ 167 168 # A snippet for grd file 169 GRD_SNIPPET_TEMPLATE=""" <message name="%s" desc="%s"> 170 %s 171 </message> 172 """ 173 174 # A snippet for C++ file 175 CC_SNIPPET_TEMPLATE=""" { "%s", %s }, 176 """ 177 178 179 def SplitBehavior(behavior): 180 """Splits the behavior to compose a message or i18n-content value. 181 182 Examples: 183 'Activate last tab' => ['Activate', 'last', 'tab'] 184 'Close tab' => ['Close', 'tab'] 185 """ 186 return [x for x in re.split('[ ()"-.,]', behavior) if len(x) > 0] 187 188 189 def ToMessageName(behavior): 190 """Composes a message name for grd file. 191 192 Examples: 193 'Activate last tab' => IDS_KEYBOARD_OVERLAY_ACTIVATE_LAST_TAB 194 'Close tab' => IDS_KEYBOARD_OVERLAY_CLOSE_TAB 195 """ 196 segments = [segment.upper() for segment in SplitBehavior(behavior)] 197 return 'IDS_KEYBOARD_OVERLAY_' + ('_'.join(segments)) 198 199 200 def ToMessageDesc(description): 201 """Composes a message description for grd file.""" 202 message_desc = 'The text in the keyboard overlay to explain the shortcut' 203 if description: 204 message_desc = '%s (%s).' % (message_desc, description) 205 else: 206 message_desc += '.' 207 return message_desc 208 209 210 def Toi18nContent(behavior): 211 """Composes a i18n-content value for HTML/JavaScript files. 212 213 Examples: 214 'Activate last tab' => keyboardOverlayActivateLastTab 215 'Close tab' => keyboardOverlayCloseTab 216 """ 217 segments = [segment.lower() for segment in SplitBehavior(behavior)] 218 result = 'keyboardOverlay' 219 for segment in segments: 220 result += segment[0].upper() + segment[1:] 221 return result 222 223 224 def ToKeys(hotkey): 225 """Converts the action value to shortcut keys used from JavaScript. 226 227 Examples: 228 'Ctrl - 9' => '9<>CTRL' 229 'Ctrl - Shift - Tab' => 'tab<>CTRL<>SHIFT' 230 """ 231 values = hotkey.split(' - ') 232 modifiers = sorted(value.upper() for value in values 233 if value in ['Shift', 'Ctrl', 'Alt', 'Search']) 234 keycode = [value.lower() for value in values 235 if value not in ['Shift', 'Ctrl', 'Alt', 'Search']] 236 # The keys which are highlighted even without modifier keys. 237 base_keys = ['backspace', 'power'] 238 if not modifiers and (keycode and keycode[0] not in base_keys): 239 return None 240 return '<>'.join(keycode + modifiers) 241 242 243 def ParseOptions(): 244 """Parses the input arguemnts and returns options.""" 245 # default_username = os.getusername() + '@google.com'; 246 default_username = '%s (at] google.com' % os.environ.get('USER') 247 parser = optparse.OptionParser() 248 parser.add_option('--key', dest='key', 249 help='The key of the spreadsheet (required).') 250 parser.add_option('--username', dest='username', 251 default=default_username, 252 help='Your user name (default: %s).' % default_username) 253 parser.add_option('--password', dest='password', 254 help='Your password.') 255 parser.add_option('--account_type', default='GOOGLE', dest='account_type', 256 help='Account type used for gdata login (default: GOOGLE)') 257 parser.add_option('--js', dest='js', default=False, action='store_true', 258 help='Output js file.') 259 parser.add_option('--grd', dest='grd', default=False, action='store_true', 260 help='Output resource file.') 261 parser.add_option('--cc', dest='cc', default=False, action='store_true', 262 help='Output cc file.') 263 parser.add_option('--outdir', dest='outdir', default=None, 264 help='Specify the directory files are generated.') 265 (options, unused_args) = parser.parse_args() 266 267 if not options.username.endswith('google.com'): 268 print 'google.com account is necessary to use this script.' 269 sys.exit(-1) 270 271 if (not (options.js or options.grd or options.cc)): 272 print 'Either --js, --grd, or --cc needs to be specified.' 273 sys.exit(-1) 274 275 # Get the password from the terminal, if needed. 276 if not options.password: 277 options.password = getpass.getpass( 278 'Application specific password for %s: ' % options.username) 279 return options 280 281 282 def InitClient(options): 283 """Initializes the spreadsheet client.""" 284 client = gdata.spreadsheet.service.SpreadsheetsService() 285 client.email = options.username 286 client.password = options.password 287 client.source = 'Spread Sheet' 288 client.account_type = options.account_type 289 print 'Logging in as %s (%s)' % (client.email, client.account_type) 290 client.ProgrammaticLogin() 291 return client 292 293 294 def PrintDiffs(message, lhs, rhs): 295 """Prints the differences between |lhs| and |rhs|.""" 296 dif = set(lhs).difference(rhs) 297 if dif: 298 print message, ', '.join(dif) 299 300 301 def FetchSpreadsheetFeeds(client, key, sheets, cols): 302 """Fetch feeds from the spreadsheet. 303 304 Args: 305 client: A spreadsheet client to be used for fetching data. 306 key: A key string of the spreadsheet to be fetched. 307 sheets: A list of the sheet names to read data from. 308 cols: A list of columns to read data from. 309 """ 310 worksheets_feed = client.GetWorksheetsFeed(key) 311 print 'Fetching data from the worksheet: %s' % worksheets_feed.title.text 312 worksheets_data = {} 313 titles = [] 314 for entry in worksheets_feed.entry: 315 worksheet_id = entry.id.text.split('/')[-1] 316 list_feed = client.GetListFeed(key, worksheet_id) 317 list_data = [] 318 # Hack to deal with sheet names like 'sv (Copy of fl)' 319 title = list_feed.title.text.split('(')[0].strip() 320 titles.append(title) 321 if title not in sheets: 322 continue 323 print 'Reading data from the sheet: %s' % list_feed.title.text 324 for i, entry in enumerate(list_feed.entry): 325 line_data = {} 326 for k in entry.custom: 327 if (k not in cols) or (not entry.custom[k].text): 328 continue 329 line_data[k] = entry.custom[k].text 330 list_data.append(line_data) 331 worksheets_data[title] = list_data 332 PrintDiffs('Exist only on the spreadsheet: ', titles, sheets) 333 PrintDiffs('Specified but do not exist on the spreadsheet: ', sheets, titles) 334 return worksheets_data 335 336 337 def FetchKeyboardGlyphData(client): 338 """Fetches the keyboard glyph data from the spreadsheet.""" 339 glyph_cols = ['scancode', 'p0', 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 340 'p8', 'p9', 'label', 'format', 'notes'] 341 keyboard_glyph_data = FetchSpreadsheetFeeds( 342 client, KEYBOARD_GLYPH_SPREADSHEET_KEY, 343 INPUT_METHOD_ID_TO_OVERLAY_ID.values(), glyph_cols) 344 ret = {} 345 for lang in keyboard_glyph_data: 346 ret[lang] = {} 347 keys = {} 348 for line in keyboard_glyph_data[lang]: 349 scancode = line.get('scancode') 350 if (not scancode) and line.get('notes'): 351 ret[lang]['layoutName'] = line['notes'] 352 continue 353 del line['scancode'] 354 if 'notes' in line: 355 del line['notes'] 356 if 'label' in line: 357 line['label'] = LABEL_MAP.get(line['label'], line['label']) 358 keys[scancode] = line 359 # Add a label to space key 360 if '39' not in keys: 361 keys['39'] = {'label': 'space'} 362 ret[lang]['keys'] = keys 363 return ret 364 365 366 def FetchLayoutsData(client): 367 """Fetches the keyboard glyph data from the spreadsheet.""" 368 layout_names = ['U_layout', 'J_layout', 'E_layout', 'B_layout'] 369 cols = ['scancode', 'x', 'y', 'w', 'h'] 370 layouts = FetchSpreadsheetFeeds(client, KEYBOARD_GLYPH_SPREADSHEET_KEY, 371 layout_names, cols) 372 ret = {} 373 for layout_name, layout in layouts.items(): 374 ret[layout_name[0]] = [] 375 for row in layout: 376 line = [] 377 for col in cols: 378 value = row.get(col) 379 if not value: 380 line.append('') 381 else: 382 if col != 'scancode': 383 value = float(value) 384 line.append(value) 385 ret[layout_name[0]].append(line) 386 return ret 387 388 389 def FetchHotkeyData(client): 390 """Fetches the hotkey data from the spreadsheet.""" 391 hotkey_sheet = ['Cross Platform Behaviors'] 392 hotkey_cols = ['behavior', 'context', 'kind', 'actionctrlctrlcmdonmac', 393 'chromeos', 'descriptionfortranslation'] 394 hotkey_data = FetchSpreadsheetFeeds(client, HOTKEY_SPREADSHEET_KEY, 395 hotkey_sheet, hotkey_cols) 396 action_to_id = {} 397 id_to_behavior = {} 398 # (behavior, action) 399 result = [] 400 for line in hotkey_data['Cross Platform Behaviors']: 401 if (not line.get('chromeos')) or (line.get('kind') != 'Key'): 402 continue 403 action = ToKeys(line['actionctrlctrlcmdonmac']) 404 if not action: 405 continue 406 behavior = line['behavior'].strip() 407 description = line.get('descriptionfortranslation') 408 result.append((behavior, action, description)) 409 return result 410 411 412 def UniqueBehaviors(hotkey_data): 413 """Retrieves a sorted list of unique behaviors from |hotkey_data|.""" 414 return sorted(set((behavior, description) for (behavior, _, description) 415 in hotkey_data), 416 cmp=lambda x, y: cmp(ToMessageName(x[0]), ToMessageName(y[0]))) 417 418 419 def GetPath(path_from_src): 420 """Returns the absolute path of the specified path.""" 421 path = os.path.join(os.path.dirname(__file__), '../..', path_from_src) 422 if not os.path.isfile(path): 423 print 'WARNING: %s does not exist. Maybe moved or renamed?' % path 424 return path 425 426 427 def OutputFile(outpath, snippet): 428 """Output the snippet into the specified path.""" 429 out = file(outpath, 'w') 430 out.write(COPYRIGHT_HEADER + '\n') 431 out.write(snippet) 432 print 'Output ' + os.path.normpath(outpath) 433 434 435 def RewriteFile(start, end, original_dir, original_filename, snippet, 436 outdir=None): 437 """Replaces a part of the specified file with snippet and outputs it.""" 438 original_path = GetPath(os.path.join(original_dir, original_filename)) 439 original = file(original_path, 'r') 440 original_content = original.read() 441 original.close() 442 if outdir: 443 outpath = os.path.join(outdir, original_filename) 444 else: 445 outpath = original_path 446 out = file(outpath, 'w') 447 rx = re.compile(r'%s\n.*?%s\n' % (re.escape(start), re.escape(end)), 448 re.DOTALL) 449 new_content = re.sub(rx, '%s\n%s%s\n' % (start, snippet, end), 450 original_content) 451 out.write(new_content) 452 out.close() 453 print 'Output ' + os.path.normpath(outpath) 454 455 456 def OutputJson(keyboard_glyph_data, hotkey_data, layouts, var_name, outdir): 457 """Outputs the keyboard overlay data as a JSON file.""" 458 action_to_id = {} 459 for (behavior, action, _) in hotkey_data: 460 i18nContent = Toi18nContent(behavior) 461 action_to_id[action] = i18nContent 462 data = {'keyboardGlyph': keyboard_glyph_data, 463 'shortcut': action_to_id, 464 'layouts': layouts, 465 'inputMethodIdToOverlayId': INPUT_METHOD_ID_TO_OVERLAY_ID} 466 467 if not outdir: 468 outdir = JS_OUTDIR 469 outpath = GetPath(os.path.join(outdir, JS_FILENAME)) 470 json_data = json.dumps(data, sort_keys=True, indent=2) 471 # Remove redundant spaces after ',' 472 json_data = json_data.replace(', \n', ',\n') 473 # Replace double quotes with single quotes to avoid lint warnings. 474 json_data = json_data.replace('\"', '\'') 475 snippet = 'var %s = %s;\n' % (var_name, json_data) 476 OutputFile(outpath, snippet) 477 478 479 def OutputGrd(hotkey_data, outdir): 480 """Outputs a part of messages in the grd file.""" 481 snippet = cStringIO.StringIO() 482 for (behavior, description) in UniqueBehaviors(hotkey_data): 483 # Do not generate message for 'Show wrench menu'. It is handled manually 484 # based on branding. 485 if behavior == 'Show wrench menu': 486 continue 487 snippet.write(GRD_SNIPPET_TEMPLATE % 488 (ToMessageName(behavior), ToMessageDesc(description), 489 behavior)) 490 491 RewriteFile(GRD_START, GRD_END, GRD_OUTDIR, GRD_FILENAME, snippet.getvalue(), 492 outdir) 493 494 495 def OutputCC(hotkey_data, outdir): 496 """Outputs a part of code in the C++ file.""" 497 snippet = cStringIO.StringIO() 498 for (behavior, _) in UniqueBehaviors(hotkey_data): 499 message_name = ToMessageName(behavior) 500 output = CC_SNIPPET_TEMPLATE % (Toi18nContent(behavior), message_name) 501 # Break the line if the line is longer than 80 characters 502 if len(output) > 80: 503 output = output.replace(' ' + message_name, '\n %s' % message_name) 504 snippet.write(output) 505 506 RewriteFile(CC_START, CC_END, CC_OUTDIR, CC_FILENAME, snippet.getvalue(), 507 outdir) 508 509 510 def main(): 511 options = ParseOptions() 512 client = InitClient(options) 513 hotkey_data = FetchHotkeyData(client) 514 515 if options.js: 516 keyboard_glyph_data = FetchKeyboardGlyphData(client) 517 518 if options.js: 519 layouts = FetchLayoutsData(client) 520 OutputJson(keyboard_glyph_data, hotkey_data, layouts, 'keyboardOverlayData', 521 options.outdir) 522 if options.grd: 523 OutputGrd(hotkey_data, options.outdir) 524 if options.cc: 525 OutputCC(hotkey_data, options.outdir) 526 527 528 if __name__ == '__main__': 529 main() 530