blob: 073ef0bfdcec880989871cb3c919ece43277c3a2 [file] [log] [blame]
#!/usr/bin/env python
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Generate keyboard layout and hotkey data for the keyboard overlay.
This script fetches data from the keyboard layout and hotkey data spreadsheet,
and output the data depending on the option.
--cc: Rewrites a part of C++ code in
chrome/browser/chromeos/webui/keyboard_overlay_ui.cc
--grd: Rewrites a part of grd messages in
chrome/app/generated_resources.grd
--js: Rewrites the entire JavaScript code in
chrome/browser/resources/keyboard_overlay/keyboard_overlay_data.js
These options can be specified at the same time.
e.g.
python gen_keyboard_overlay_data.py --cc --grd --js
The output directory of the generated files can be changed with --outdir.
e.g. (This will generate tmp/keyboard_overlay.js)
python gen_keyboard_overlay_data.py --outdir=tmp --js
"""
import cStringIO
import datetime
import gdata.spreadsheet.service
import getpass
import json
import optparse
import os
import re
import sys
MODIFIER_SHIFT = 1 << 0
MODIFIER_CTRL = 1 << 1
MODIFIER_ALT = 1 << 2
KEYBOARD_GLYPH_SPREADSHEET_KEY = '0Ao3KldW9piwEdExLbGR6TmZ2RU9aUjFCMmVxWkVqVmc'
HOTKEY_SPREADSHEET_KEY = '0AqzoqbAMLyEPdE1RQXdodk1qVkFyTWtQbUxROVM1cXc'
CC_OUTDIR = 'chrome/browser/ui/webui/chromeos'
CC_FILENAME = 'keyboard_overlay_ui.cc'
GRD_OUTDIR = 'chrome/app'
GRD_FILENAME = 'chromeos_strings.grdp'
JS_OUTDIR = 'chrome/browser/resources/chromeos'
JS_FILENAME = 'keyboard_overlay_data.js'
CC_START = r'IDS_KEYBOARD_OVERLAY_INSTRUCTIONS_HIDE },'
CC_END = r'};'
GRD_START = r' <!-- BEGIN GENERATED KEYBOARD OVERLAY STRINGS -->'
GRD_END = r' <!-- END GENERATED KEYBOARD OVERLAY STRINGS -->'
LABEL_MAP = {
'glyph_arrow_down': 'down',
'glyph_arrow_left': 'left',
'glyph_arrow_right': 'right',
'glyph_arrow_up': 'up',
'glyph_back': 'back',
'glyph_backspace': 'backspace',
'glyph_brightness_down': 'bright down',
'glyph_brightness_up': 'bright up',
'glyph_enter': 'enter',
'glyph_forward': 'forward',
'glyph_fullscreen': 'full screen',
# Kana/Eisu key on Japanese keyboard
'glyph_ime': u'\u304b\u306a\u0020\u002f\u0020\u82f1\u6570',
'glyph_lock': 'lock',
'glyph_overview': 'switch window',
'glyph_power': 'power',
'glyph_right': 'right',
'glyph_reload': 'reload',
'glyph_search': 'search',
'glyph_shift': 'shift',
'glyph_tab': 'tab',
'glyph_tools': 'tools',
'glyph_volume_down': 'vol. down',
'glyph_volume_mute': 'mute',
'glyph_volume_up': 'vol. up',
};
INPUT_METHOD_ID_TO_OVERLAY_ID = {
'xkb:be::fra': 'fr',
'xkb:be::ger': 'de',
'xkb:be::nld': 'nl',
'xkb:bg::bul': 'bg',
'xkb:bg:phonetic:bul': 'bg',
'xkb:br::por': 'pt_BR',
'xkb:ca::fra': 'fr_CA',
'xkb:ca:eng:eng': 'ca',
'xkb:ch::ger': 'de',
'xkb:ch:fr:fra': 'fr',
'xkb:cz::cze': 'cs',
'xkb:de::ger': 'de',
'xkb:de:neo:ger': 'de_neo',
'xkb:dk::dan': 'da',
'xkb:ee::est': 'et',
'xkb:es::spa': 'es',
'xkb:es:cat:cat': 'ca',
'xkb:fi::fin': 'fi',
'xkb:fr::fra': 'fr',
'xkb:gb:dvorak:eng': 'en_GB_dvorak',
'xkb:gb:extd:eng': 'en_GB',
'xkb:gr::gre': 'el',
'xkb:hr::scr': 'hr',
'xkb:hu::hun': 'hu',
'xkb:il::heb': 'iw',
'xkb:it::ita': 'it',
'xkb:jp::jpn': 'ja',
'xkb:latam::spa': 'es_419',
'xkb:lt::lit': 'lt',
'xkb:lv:apostrophe:lav': 'lv',
'xkb:no::nob': 'no',
'xkb:pl::pol': 'pl',
'xkb:pt::por': 'pt_PT',
'xkb:ro::rum': 'ro',
'xkb:rs::srp': 'sr',
'xkb:ru::rus': 'ru',
'xkb:ru:phonetic:rus': 'ru',
'xkb:se::swe': 'sv',
'xkb:si::slv': 'sl',
'xkb:sk::slo': 'sk',
'xkb:tr::tur': 'tr',
'xkb:ua::ukr': 'uk',
'xkb:us::eng': 'en_US',
'xkb:us::fil': 'en_US',
'xkb:us::ind': 'en_US',
'xkb:us::msa': 'en_US',
'xkb:us:altgr-intl:eng': 'en_US_altgr_intl',
'xkb:us:colemak:eng': 'en_US_colemak',
'xkb:us:dvorak:eng': 'en_US_dvorak',
'xkb:us:intl:eng': 'en_US_intl',
'xkb:us:intl:nld': 'en_US_intl',
'xkb:us:intl:por': 'en_US_intl',
'xkb:us:workman:eng': 'en_US_workman',
'xkb:us:workman-intl:eng': 'en_US_workman_intl',
}
# The file was first generated in 2012 and we have a policy of not updating
# copyright dates.
COPYRIGHT_HEADER=\
"""// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This is a generated file but may contain local modifications. See
// src/tools/gen_keyboard_overlay_data/gen_keyboard_overlay_data.py --help
"""
# A snippet for grd file
GRD_SNIPPET_TEMPLATE=""" <message name="%s" desc="%s">
%s
</message>
"""
# A snippet for C++ file
CC_SNIPPET_TEMPLATE=""" { "%s", %s },
"""
def SplitBehavior(behavior):
"""Splits the behavior to compose a message or i18n-content value.
Examples:
'Activate last tab' => ['Activate', 'last', 'tab']
'Close tab' => ['Close', 'tab']
"""
return [x for x in re.split('[ ()"-.,]', behavior) if len(x) > 0]
def ToMessageName(behavior):
"""Composes a message name for grd file.
Examples:
'Activate last tab' => IDS_KEYBOARD_OVERLAY_ACTIVATE_LAST_TAB
'Close tab' => IDS_KEYBOARD_OVERLAY_CLOSE_TAB
"""
segments = [segment.upper() for segment in SplitBehavior(behavior)]
return 'IDS_KEYBOARD_OVERLAY_' + ('_'.join(segments))
def ToMessageDesc(description):
"""Composes a message description for grd file."""
message_desc = 'The text in the keyboard overlay to explain the shortcut'
if description:
message_desc = '%s (%s).' % (message_desc, description)
else:
message_desc += '.'
return message_desc
def Toi18nContent(behavior):
"""Composes a i18n-content value for HTML/JavaScript files.
Examples:
'Activate last tab' => keyboardOverlayActivateLastTab
'Close tab' => keyboardOverlayCloseTab
"""
segments = [segment.lower() for segment in SplitBehavior(behavior)]
result = 'keyboardOverlay'
for segment in segments:
result += segment[0].upper() + segment[1:]
return result
def ToKeys(hotkey):
"""Converts the action value to shortcut keys used from JavaScript.
Examples:
'Ctrl - 9' => '9<>CTRL'
'Ctrl - Shift - Tab' => 'tab<>CTRL<>SHIFT'
"""
values = hotkey.split(' - ')
modifiers = sorted(value.upper() for value in values
if value in ['Shift', 'Ctrl', 'Alt', 'Search'])
keycode = [value.lower() for value in values
if value not in ['Shift', 'Ctrl', 'Alt', 'Search']]
# The keys which are highlighted even without modifier keys.
base_keys = ['backspace', 'power']
if not modifiers and (keycode and keycode[0] not in base_keys):
return None
return '<>'.join(keycode + modifiers)
def ParseOptions():
"""Parses the input arguemnts and returns options."""
# default_username = os.getusername() + '@google.com';
default_username = '%s@google.com' % os.environ.get('USER')
parser = optparse.OptionParser()
parser.add_option('--key', dest='key',
help='The key of the spreadsheet (required).')
parser.add_option('--username', dest='username',
default=default_username,
help='Your user name (default: %s).' % default_username)
parser.add_option('--password', dest='password',
help='Your password.')
parser.add_option('--account_type', default='GOOGLE', dest='account_type',
help='Account type used for gdata login (default: GOOGLE)')
parser.add_option('--js', dest='js', default=False, action='store_true',
help='Output js file.')
parser.add_option('--grd', dest='grd', default=False, action='store_true',
help='Output resource file.')
parser.add_option('--cc', dest='cc', default=False, action='store_true',
help='Output cc file.')
parser.add_option('--outdir', dest='outdir', default=None,
help='Specify the directory files are generated.')
(options, unused_args) = parser.parse_args()
if not options.username.endswith('google.com'):
print 'google.com account is necessary to use this script.'
sys.exit(-1)
if (not (options.js or options.grd or options.cc)):
print 'Either --js, --grd, or --cc needs to be specified.'
sys.exit(-1)
# Get the password from the terminal, if needed.
if not options.password:
options.password = getpass.getpass(
'Application specific password for %s: ' % options.username)
return options
def InitClient(options):
"""Initializes the spreadsheet client."""
client = gdata.spreadsheet.service.SpreadsheetsService()
client.email = options.username
client.password = options.password
client.source = 'Spread Sheet'
client.account_type = options.account_type
print 'Logging in as %s (%s)' % (client.email, client.account_type)
client.ProgrammaticLogin()
return client
def PrintDiffs(message, lhs, rhs):
"""Prints the differences between |lhs| and |rhs|."""
dif = set(lhs).difference(rhs)
if dif:
print message, ', '.join(dif)
def FetchSpreadsheetFeeds(client, key, sheets, cols):
"""Fetch feeds from the spreadsheet.
Args:
client: A spreadsheet client to be used for fetching data.
key: A key string of the spreadsheet to be fetched.
sheets: A list of the sheet names to read data from.
cols: A list of columns to read data from.
"""
worksheets_feed = client.GetWorksheetsFeed(key)
print 'Fetching data from the worksheet: %s' % worksheets_feed.title.text
worksheets_data = {}
titles = []
for entry in worksheets_feed.entry:
worksheet_id = entry.id.text.split('/')[-1]
list_feed = client.GetListFeed(key, worksheet_id)
list_data = []
# Hack to deal with sheet names like 'sv (Copy of fl)'
title = list_feed.title.text.split('(')[0].strip()
titles.append(title)
if title not in sheets:
continue
print 'Reading data from the sheet: %s' % list_feed.title.text
for i, entry in enumerate(list_feed.entry):
line_data = {}
for k in entry.custom:
if (k not in cols) or (not entry.custom[k].text):
continue
line_data[k] = entry.custom[k].text
list_data.append(line_data)
worksheets_data[title] = list_data
PrintDiffs('Exist only on the spreadsheet: ', titles, sheets)
PrintDiffs('Specified but do not exist on the spreadsheet: ', sheets, titles)
return worksheets_data
def FetchKeyboardGlyphData(client):
"""Fetches the keyboard glyph data from the spreadsheet."""
glyph_cols = ['scancode', 'p0', 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7',
'p8', 'p9', 'label', 'format', 'notes']
keyboard_glyph_data = FetchSpreadsheetFeeds(
client, KEYBOARD_GLYPH_SPREADSHEET_KEY,
INPUT_METHOD_ID_TO_OVERLAY_ID.values(), glyph_cols)
ret = {}
for lang in keyboard_glyph_data:
ret[lang] = {}
keys = {}
for line in keyboard_glyph_data[lang]:
scancode = line.get('scancode')
if (not scancode) and line.get('notes'):
ret[lang]['layoutName'] = line['notes']
continue
del line['scancode']
if 'notes' in line:
del line['notes']
if 'label' in line:
line['label'] = LABEL_MAP.get(line['label'], line['label'])
keys[scancode] = line
# Add a label to space key
if '39' not in keys:
keys['39'] = {'label': 'space'}
ret[lang]['keys'] = keys
return ret
def FetchLayoutsData(client):
"""Fetches the keyboard glyph data from the spreadsheet."""
layout_names = ['U_layout', 'J_layout', 'E_layout', 'B_layout']
cols = ['scancode', 'x', 'y', 'w', 'h']
layouts = FetchSpreadsheetFeeds(client, KEYBOARD_GLYPH_SPREADSHEET_KEY,
layout_names, cols)
ret = {}
for layout_name, layout in layouts.items():
ret[layout_name[0]] = []
for row in layout:
line = []
for col in cols:
value = row.get(col)
if not value:
line.append('')
else:
if col != 'scancode':
value = float(value)
line.append(value)
ret[layout_name[0]].append(line)
return ret
def FetchHotkeyData(client):
"""Fetches the hotkey data from the spreadsheet."""
hotkey_sheet = ['Cross Platform Behaviors']
hotkey_cols = ['behavior', 'context', 'kind', 'actionctrlctrlcmdonmac',
'chromeos', 'descriptionfortranslation']
hotkey_data = FetchSpreadsheetFeeds(client, HOTKEY_SPREADSHEET_KEY,
hotkey_sheet, hotkey_cols)
action_to_id = {}
id_to_behavior = {}
# (behavior, action)
result = []
for line in hotkey_data['Cross Platform Behaviors']:
if (not line.get('chromeos')) or (line.get('kind') != 'Key'):
continue
action = ToKeys(line['actionctrlctrlcmdonmac'])
if not action:
continue
behavior = line['behavior'].strip()
description = line.get('descriptionfortranslation')
result.append((behavior, action, description))
return result
def UniqueBehaviors(hotkey_data):
"""Retrieves a sorted list of unique behaviors from |hotkey_data|."""
return sorted(set((behavior, description) for (behavior, _, description)
in hotkey_data),
cmp=lambda x, y: cmp(ToMessageName(x[0]), ToMessageName(y[0])))
def GetPath(path_from_src):
"""Returns the absolute path of the specified path."""
path = os.path.join(os.path.dirname(__file__), '../..', path_from_src)
if not os.path.isfile(path):
print 'WARNING: %s does not exist. Maybe moved or renamed?' % path
return path
def OutputFile(outpath, snippet):
"""Output the snippet into the specified path."""
out = file(outpath, 'w')
out.write(COPYRIGHT_HEADER + '\n')
out.write(snippet)
print 'Output ' + os.path.normpath(outpath)
def RewriteFile(start, end, original_dir, original_filename, snippet,
outdir=None):
"""Replaces a part of the specified file with snippet and outputs it."""
original_path = GetPath(os.path.join(original_dir, original_filename))
original = file(original_path, 'r')
original_content = original.read()
original.close()
if outdir:
outpath = os.path.join(outdir, original_filename)
else:
outpath = original_path
out = file(outpath, 'w')
rx = re.compile(r'%s\n.*?%s\n' % (re.escape(start), re.escape(end)),
re.DOTALL)
new_content = re.sub(rx, '%s\n%s%s\n' % (start, snippet, end),
original_content)
out.write(new_content)
out.close()
print 'Output ' + os.path.normpath(outpath)
def OutputJson(keyboard_glyph_data, hotkey_data, layouts, var_name, outdir):
"""Outputs the keyboard overlay data as a JSON file."""
action_to_id = {}
for (behavior, action, _) in hotkey_data:
i18nContent = Toi18nContent(behavior)
action_to_id[action] = i18nContent
data = {'keyboardGlyph': keyboard_glyph_data,
'shortcut': action_to_id,
'layouts': layouts,
'inputMethodIdToOverlayId': INPUT_METHOD_ID_TO_OVERLAY_ID}
if not outdir:
outdir = JS_OUTDIR
outpath = GetPath(os.path.join(outdir, JS_FILENAME))
json_data = json.dumps(data, sort_keys=True, indent=2)
# Remove redundant spaces after ','
json_data = json_data.replace(', \n', ',\n')
# Replace double quotes with single quotes to avoid lint warnings.
json_data = json_data.replace('\"', '\'')
snippet = 'var %s = %s;\n' % (var_name, json_data)
OutputFile(outpath, snippet)
def OutputGrd(hotkey_data, outdir):
"""Outputs a part of messages in the grd file."""
snippet = cStringIO.StringIO()
for (behavior, description) in UniqueBehaviors(hotkey_data):
# Do not generate message for 'Show wrench menu'. It is handled manually
# based on branding.
if behavior == 'Show wrench menu':
continue
snippet.write(GRD_SNIPPET_TEMPLATE %
(ToMessageName(behavior), ToMessageDesc(description),
behavior))
RewriteFile(GRD_START, GRD_END, GRD_OUTDIR, GRD_FILENAME, snippet.getvalue(),
outdir)
def OutputCC(hotkey_data, outdir):
"""Outputs a part of code in the C++ file."""
snippet = cStringIO.StringIO()
for (behavior, _) in UniqueBehaviors(hotkey_data):
message_name = ToMessageName(behavior)
output = CC_SNIPPET_TEMPLATE % (Toi18nContent(behavior), message_name)
# Break the line if the line is longer than 80 characters
if len(output) > 80:
output = output.replace(' ' + message_name, '\n %s' % message_name)
snippet.write(output)
RewriteFile(CC_START, CC_END, CC_OUTDIR, CC_FILENAME, snippet.getvalue(),
outdir)
def main():
options = ParseOptions()
client = InitClient(options)
hotkey_data = FetchHotkeyData(client)
if options.js:
keyboard_glyph_data = FetchKeyboardGlyphData(client)
if options.js:
layouts = FetchLayoutsData(client)
OutputJson(keyboard_glyph_data, hotkey_data, layouts, 'keyboardOverlayData',
options.outdir)
if options.grd:
OutputGrd(hotkey_data, options.outdir)
if options.cc:
OutputCC(hotkey_data, options.outdir)
if __name__ == '__main__':
main()