Home | History | Annotate | Download | only in gen_keyboard_overlay_data
      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   'xkb:be::fra': 'fr',
     88   'xkb:be::ger': 'de',
     89   'xkb:be::nld': 'nl',
     90   'xkb:bg::bul': 'bg',
     91   'xkb:bg:phonetic:bul': 'bg',
     92   'xkb:br::por': 'pt_BR',
     93   'xkb:ca::fra': 'fr_CA',
     94   'xkb:ca:eng:eng': 'ca',
     95   'xkb:ch::ger': 'de',
     96   'xkb:ch:fr:fra': 'fr',
     97   'xkb:cz::cze': 'cs',
     98   'xkb:de::ger': 'de',
     99   'xkb:de:neo:ger': 'de_neo',
    100   'xkb:dk::dan': 'da',
    101   'xkb:ee::est': 'et',
    102   'xkb:es::spa': 'es',
    103   'xkb:es:cat:cat': 'ca',
    104   'xkb:fi::fin': 'fi',
    105   'xkb:fr::fra': 'fr',
    106   'xkb:gb:dvorak:eng': 'en_GB_dvorak',
    107   'xkb:gb:extd:eng': 'en_GB',
    108   'xkb:gr::gre': 'el',
    109   'xkb:hr::scr': 'hr',
    110   'xkb:hu::hun': 'hu',
    111   'xkb:il::heb': 'iw',
    112   'xkb:it::ita': 'it',
    113   'xkb:jp::jpn': 'ja',
    114   'xkb:latam::spa': 'es_419',
    115   'xkb:lt::lit': 'lt',
    116   'xkb:lv:apostrophe:lav': 'lv',
    117   'xkb:no::nob': 'no',
    118   'xkb:pl::pol': 'pl',
    119   'xkb:pt::por': 'pt_PT',
    120   'xkb:ro::rum': 'ro',
    121   'xkb:rs::srp': 'sr',
    122   'xkb:ru::rus': 'ru',
    123   'xkb:ru:phonetic:rus': 'ru',
    124   'xkb:se::swe': 'sv',
    125   'xkb:si::slv': 'sl',
    126   'xkb:sk::slo': 'sk',
    127   'xkb:tr::tur': 'tr',
    128   'xkb:ua::ukr': 'uk',
    129   'xkb:us::eng': 'en_US',
    130   'xkb:us::fil': 'en_US',
    131   'xkb:us::ind': 'en_US',
    132   'xkb:us::msa': 'en_US',
    133   'xkb:us:altgr-intl:eng': 'en_US_altgr_intl',
    134   'xkb:us:colemak:eng': 'en_US_colemak',
    135   'xkb:us:dvorak:eng': 'en_US_dvorak',
    136   'xkb:us:intl:eng': 'en_US_intl',
    137   'xkb:us:intl:nld': 'en_US_intl',
    138   'xkb:us:intl:por': 'en_US_intl'
    139 }
    140 
    141 # The file was first generated in 2012 and we have a policy of not updating
    142 # copyright dates.
    143 COPYRIGHT_HEADER=\
    144 """// Copyright (c) 2012 The Chromium Authors. All rights reserved.
    145 // Use of this source code is governed by a BSD-style license that can be
    146 // found in the LICENSE file.
    147 
    148 // This is a generated file but may contain local modifications. See
    149 // src/tools/gen_keyboard_overlay_data/gen_keyboard_overlay_data.py --help
    150 """
    151 
    152 # A snippet for grd file
    153 GRD_SNIPPET_TEMPLATE="""  <message name="%s" desc="%s">
    154     %s
    155   </message>
    156 """
    157 
    158 # A snippet for C++ file
    159 CC_SNIPPET_TEMPLATE="""  { "%s", %s },
    160 """
    161 
    162 
    163 def SplitBehavior(behavior):
    164   """Splits the behavior to compose a message or i18n-content value.
    165 
    166   Examples:
    167     'Activate last tab' => ['Activate', 'last', 'tab']
    168     'Close tab' => ['Close', 'tab']
    169   """
    170   return [x for x in re.split('[ ()"-.,]', behavior) if len(x) > 0]
    171 
    172 
    173 def ToMessageName(behavior):
    174   """Composes a message name for grd file.
    175 
    176   Examples:
    177     'Activate last tab' => IDS_KEYBOARD_OVERLAY_ACTIVATE_LAST_TAB
    178     'Close tab' => IDS_KEYBOARD_OVERLAY_CLOSE_TAB
    179   """
    180   segments = [segment.upper() for segment in SplitBehavior(behavior)]
    181   return 'IDS_KEYBOARD_OVERLAY_' + ('_'.join(segments))
    182 
    183 
    184 def ToMessageDesc(description):
    185   """Composes a message description for grd file."""
    186   message_desc = 'The text in the keyboard overlay to explain the shortcut'
    187   if description:
    188     message_desc = '%s (%s).' % (message_desc, description)
    189   else:
    190     message_desc += '.'
    191   return message_desc
    192 
    193 
    194 def Toi18nContent(behavior):
    195   """Composes a i18n-content value for HTML/JavaScript files.
    196 
    197   Examples:
    198     'Activate last tab' => keyboardOverlayActivateLastTab
    199     'Close tab' => keyboardOverlayCloseTab
    200   """
    201   segments = [segment.lower() for segment in SplitBehavior(behavior)]
    202   result = 'keyboardOverlay'
    203   for segment in segments:
    204     result += segment[0].upper() + segment[1:]
    205   return result
    206 
    207 
    208 def ToKeys(hotkey):
    209   """Converts the action value to shortcut keys used from JavaScript.
    210 
    211   Examples:
    212     'Ctrl - 9' => '9<>CTRL'
    213     'Ctrl - Shift - Tab' => 'tab<>CTRL<>SHIFT'
    214   """
    215   values = hotkey.split(' - ')
    216   modifiers = sorted(value.upper() for value in values
    217                      if value in ['Shift', 'Ctrl', 'Alt', 'Search'])
    218   keycode = [value.lower() for value in values
    219              if value not in ['Shift', 'Ctrl', 'Alt', 'Search']]
    220   # The keys which are highlighted even without modifier keys.
    221   base_keys = ['backspace', 'power']
    222   if not modifiers and (keycode and keycode[0] not in base_keys):
    223     return None
    224   return '<>'.join(keycode + modifiers)
    225 
    226 
    227 def ParseOptions():
    228   """Parses the input arguemnts and returns options."""
    229   # default_username = os.getusername() + '@google.com';
    230   default_username = '%s (at] google.com' % os.environ.get('USER')
    231   parser = optparse.OptionParser()
    232   parser.add_option('--key', dest='key',
    233                     help='The key of the spreadsheet (required).')
    234   parser.add_option('--username', dest='username',
    235                     default=default_username,
    236                     help='Your user name (default: %s).' % default_username)
    237   parser.add_option('--password', dest='password',
    238                     help='Your password.')
    239   parser.add_option('--account_type', default='GOOGLE', dest='account_type',
    240                     help='Account type used for gdata login (default: GOOGLE)')
    241   parser.add_option('--js', dest='js', default=False, action='store_true',
    242                     help='Output js file.')
    243   parser.add_option('--grd', dest='grd', default=False, action='store_true',
    244                     help='Output resource file.')
    245   parser.add_option('--cc', dest='cc', default=False, action='store_true',
    246                     help='Output cc file.')
    247   parser.add_option('--outdir', dest='outdir', default=None,
    248                     help='Specify the directory files are generated.')
    249   (options, unused_args) = parser.parse_args()
    250 
    251   if not options.username.endswith('google.com'):
    252     print 'google.com account is necessary to use this script.'
    253     sys.exit(-1)
    254 
    255   if (not (options.js or options.grd or options.cc)):
    256     print 'Either --js, --grd, or --cc needs to be specified.'
    257     sys.exit(-1)
    258 
    259   # Get the password from the terminal, if needed.
    260   if not options.password:
    261     options.password = getpass.getpass(
    262         'Application specific password for %s: ' % options.username)
    263   return options
    264 
    265 
    266 def InitClient(options):
    267   """Initializes the spreadsheet client."""
    268   client = gdata.spreadsheet.service.SpreadsheetsService()
    269   client.email = options.username
    270   client.password = options.password
    271   client.source = 'Spread Sheet'
    272   client.account_type = options.account_type
    273   print 'Logging in as %s (%s)' % (client.email, client.account_type)
    274   client.ProgrammaticLogin()
    275   return client
    276 
    277 
    278 def PrintDiffs(message, lhs, rhs):
    279   """Prints the differences between |lhs| and |rhs|."""
    280   dif = set(lhs).difference(rhs)
    281   if dif:
    282     print message, ', '.join(dif)
    283 
    284 
    285 def FetchSpreadsheetFeeds(client, key, sheets, cols):
    286   """Fetch feeds from the spreadsheet.
    287 
    288   Args:
    289     client: A spreadsheet client to be used for fetching data.
    290     key: A key string of the spreadsheet to be fetched.
    291     sheets: A list of the sheet names to read data from.
    292     cols: A list of columns to read data from.
    293   """
    294   worksheets_feed = client.GetWorksheetsFeed(key)
    295   print 'Fetching data from the worksheet: %s' % worksheets_feed.title.text
    296   worksheets_data = {}
    297   titles = []
    298   for entry in worksheets_feed.entry:
    299     worksheet_id = entry.id.text.split('/')[-1]
    300     list_feed = client.GetListFeed(key, worksheet_id)
    301     list_data = []
    302     # Hack to deal with sheet names like 'sv (Copy of fl)'
    303     title = list_feed.title.text.split('(')[0].strip()
    304     titles.append(title)
    305     if title not in sheets:
    306       continue
    307     print 'Reading data from the sheet: %s' % list_feed.title.text
    308     for i, entry in enumerate(list_feed.entry):
    309       line_data = {}
    310       for k in entry.custom:
    311         if (k not in cols) or (not entry.custom[k].text):
    312           continue
    313         line_data[k] = entry.custom[k].text
    314       list_data.append(line_data)
    315     worksheets_data[title] = list_data
    316   PrintDiffs('Exist only on the spreadsheet: ', titles, sheets)
    317   PrintDiffs('Specified but do not exist on the spreadsheet: ', sheets, titles)
    318   return worksheets_data
    319 
    320 
    321 def FetchKeyboardGlyphData(client):
    322   """Fetches the keyboard glyph data from the spreadsheet."""
    323   glyph_cols = ['scancode', 'p0', 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7',
    324                 'p8', 'p9', 'label', 'format', 'notes']
    325   keyboard_glyph_data = FetchSpreadsheetFeeds(
    326       client, KEYBOARD_GLYPH_SPREADSHEET_KEY,
    327       INPUT_METHOD_ID_TO_OVERLAY_ID.values(), glyph_cols)
    328   ret = {}
    329   for lang in keyboard_glyph_data:
    330     ret[lang] = {}
    331     keys = {}
    332     for line in keyboard_glyph_data[lang]:
    333       scancode = line.get('scancode')
    334       if (not scancode) and line.get('notes'):
    335         ret[lang]['layoutName'] = line['notes']
    336         continue
    337       del line['scancode']
    338       if 'notes' in line:
    339         del line['notes']
    340       if 'label' in line:
    341         line['label'] = LABEL_MAP.get(line['label'], line['label'])
    342       keys[scancode] = line
    343     # Add a label to space key
    344     if '39' not in keys:
    345       keys['39'] = {'label': 'space'}
    346     ret[lang]['keys'] = keys
    347   return ret
    348 
    349 
    350 def FetchLayoutsData(client):
    351   """Fetches the keyboard glyph data from the spreadsheet."""
    352   layout_names = ['U_layout', 'J_layout', 'E_layout', 'B_layout']
    353   cols = ['scancode', 'x', 'y', 'w', 'h']
    354   layouts = FetchSpreadsheetFeeds(client, KEYBOARD_GLYPH_SPREADSHEET_KEY,
    355                                   layout_names, cols)
    356   ret = {}
    357   for layout_name, layout in layouts.items():
    358     ret[layout_name[0]] = []
    359     for row in layout:
    360       line = []
    361       for col in cols:
    362         value = row.get(col)
    363         if not value:
    364           line.append('')
    365         else:
    366           if col != 'scancode':
    367             value = float(value)
    368           line.append(value)
    369       ret[layout_name[0]].append(line)
    370   return ret
    371 
    372 
    373 def FetchHotkeyData(client):
    374   """Fetches the hotkey data from the spreadsheet."""
    375   hotkey_sheet = ['Cross Platform Behaviors']
    376   hotkey_cols = ['behavior', 'context', 'kind', 'actionctrlctrlcmdonmac',
    377                  'chromeos', 'descriptionfortranslation']
    378   hotkey_data = FetchSpreadsheetFeeds(client, HOTKEY_SPREADSHEET_KEY,
    379                                       hotkey_sheet, hotkey_cols)
    380   action_to_id = {}
    381   id_to_behavior = {}
    382   # (behavior, action)
    383   result = []
    384   for line in hotkey_data['Cross Platform Behaviors']:
    385     if (not line.get('chromeos')) or (line.get('kind') != 'Key'):
    386       continue
    387     action = ToKeys(line['actionctrlctrlcmdonmac'])
    388     if not action:
    389       continue
    390     behavior = line['behavior'].strip()
    391     description = line.get('descriptionfortranslation')
    392     result.append((behavior, action, description))
    393   return result
    394 
    395 
    396 def UniqueBehaviors(hotkey_data):
    397   """Retrieves a sorted list of unique behaviors from |hotkey_data|."""
    398   return sorted(set((behavior, description) for (behavior, _, description)
    399                     in hotkey_data),
    400                 cmp=lambda x, y: cmp(ToMessageName(x[0]), ToMessageName(y[0])))
    401 
    402 
    403 def GetPath(path_from_src):
    404   """Returns the absolute path of the specified path."""
    405   path = os.path.join(os.path.dirname(__file__), '../..', path_from_src)
    406   if not os.path.isfile(path):
    407     print 'WARNING: %s does not exist. Maybe moved or renamed?' % path
    408   return path
    409 
    410 
    411 def OutputFile(outpath, snippet):
    412   """Output the snippet into the specified path."""
    413   out = file(outpath, 'w')
    414   out.write(COPYRIGHT_HEADER + '\n')
    415   out.write(snippet)
    416   print 'Output ' + os.path.normpath(outpath)
    417 
    418 
    419 def RewriteFile(start, end, original_dir, original_filename, snippet,
    420                 outdir=None):
    421   """Replaces a part of the specified file with snippet and outputs it."""
    422   original_path = GetPath(os.path.join(original_dir, original_filename))
    423   original = file(original_path, 'r')
    424   original_content = original.read()
    425   original.close()
    426   if outdir:
    427     outpath = os.path.join(outdir, original_filename)
    428   else:
    429     outpath = original_path
    430   out = file(outpath, 'w')
    431   rx = re.compile(r'%s\n.*?%s\n' % (re.escape(start), re.escape(end)),
    432                   re.DOTALL)
    433   new_content = re.sub(rx, '%s\n%s%s\n' % (start, snippet, end),
    434                        original_content)
    435   out.write(new_content)
    436   out.close()
    437   print 'Output ' + os.path.normpath(outpath)
    438 
    439 
    440 def OutputJson(keyboard_glyph_data, hotkey_data, layouts, var_name, outdir):
    441   """Outputs the keyboard overlay data as a JSON file."""
    442   action_to_id = {}
    443   for (behavior, action, _) in hotkey_data:
    444     i18nContent = Toi18nContent(behavior)
    445     action_to_id[action] = i18nContent
    446   data = {'keyboardGlyph': keyboard_glyph_data,
    447           'shortcut': action_to_id,
    448           'layouts': layouts,
    449           'inputMethodIdToOverlayId': INPUT_METHOD_ID_TO_OVERLAY_ID}
    450 
    451   if not outdir:
    452     outdir = JS_OUTDIR
    453   outpath = GetPath(os.path.join(outdir, JS_FILENAME))
    454   json_data =  json.dumps(data, sort_keys=True, indent=2)
    455   # Remove redundant spaces after ','
    456   json_data = json_data.replace(', \n', ',\n')
    457   # Replace double quotes with single quotes to avoid lint warnings.
    458   json_data = json_data.replace('\"', '\'')
    459   snippet = 'var %s = %s;\n' % (var_name, json_data)
    460   OutputFile(outpath, snippet)
    461 
    462 
    463 def OutputGrd(hotkey_data, outdir):
    464   """Outputs a part of messages in the grd file."""
    465   snippet = cStringIO.StringIO()
    466   for (behavior, description) in UniqueBehaviors(hotkey_data):
    467     # Do not generate message for 'Show wrench menu'. It is handled manually
    468     # based on branding.
    469     if behavior == 'Show wrench menu':
    470       continue
    471     snippet.write(GRD_SNIPPET_TEMPLATE %
    472                   (ToMessageName(behavior), ToMessageDesc(description),
    473                    behavior))
    474 
    475   RewriteFile(GRD_START, GRD_END, GRD_OUTDIR, GRD_FILENAME, snippet.getvalue(),
    476               outdir)
    477 
    478 
    479 def OutputCC(hotkey_data, outdir):
    480   """Outputs a part of code in the C++ file."""
    481   snippet = cStringIO.StringIO()
    482   for (behavior, _) in UniqueBehaviors(hotkey_data):
    483     message_name = ToMessageName(behavior)
    484     output = CC_SNIPPET_TEMPLATE % (Toi18nContent(behavior), message_name)
    485     # Break the line if the line is longer than 80 characters
    486     if len(output) > 80:
    487       output = output.replace(' ' + message_name, '\n    %s' % message_name)
    488     snippet.write(output)
    489 
    490   RewriteFile(CC_START, CC_END, CC_OUTDIR, CC_FILENAME, snippet.getvalue(),
    491               outdir)
    492 
    493 
    494 def main():
    495   options = ParseOptions()
    496   client = InitClient(options)
    497   hotkey_data = FetchHotkeyData(client)
    498 
    499   if options.js:
    500     keyboard_glyph_data = FetchKeyboardGlyphData(client)
    501 
    502   if options.js:
    503     layouts = FetchLayoutsData(client)
    504     OutputJson(keyboard_glyph_data, hotkey_data, layouts, 'keyboardOverlayData',
    505                options.outdir)
    506   if options.grd:
    507     OutputGrd(hotkey_data, options.outdir)
    508   if options.cc:
    509     OutputCC(hotkey_data, options.outdir)
    510 
    511 
    512 if __name__ == '__main__':
    513   main()
    514