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.
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
36 import gdata
.spreadsheet
.service
44 MODIFIER_SHIFT
= 1 << 0
45 MODIFIER_CTRL
= 1 << 1
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 },'
60 GRD_START
= """Escape to hide
65 'glyph_arrow_down': 'down',
66 'glyph_arrow_left': 'left',
67 'glyph_arrow_right': 'right',
68 'glyph_arrow_up': 'up',
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',
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',
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
= {
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',
100 'm17n:vi:telex': 'vi',
101 'm17n:vi:viqr': 'vi',
103 'm17n:zh:cangjie': 'zh_TW',
104 'm17n:zh:quick': 'zh_TW',
106 'mozc-chewing': 'zh_TW',
107 'mozc-dv': 'en_US_dvorak',
111 'pinyin-dv': 'en_US_dvorak',
116 'xkb:bg:phonetic:bul': 'bg',
117 'xkb:br::por': 'pt_BR',
118 'xkb:ca::fra': 'fr_CA',
119 'xkb:ca:eng:eng': 'ca',
121 'xkb:ch:fr:fra': 'fr',
124 'xkb:de:neo:ger': 'de_neo',
128 'xkb:es:cat:cat': 'ca',
131 'xkb:gb:dvorak:eng': 'en_GB_dvorak',
132 'xkb:gb:extd:eng': 'en_GB',
139 'xkb:kr:kr104:kor': 'ko',
140 'xkb:latam::spa': 'es_419',
142 'xkb:lv:apostrophe:lav': 'lv',
145 'xkb:pt::por': 'pt_PT',
149 'xkb:ru:phonetic:rus': 'ru',
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.
169 # A snippet for grd file
170 GRD_SNIPPET_TEMPLATE
=""" <message name="%s" desc="%s">
175 # A snippet for C++ file
176 CC_SNIPPET_TEMPLATE
=""" { "%s", %s },
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_
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[] = {
201 #endif // CHROME_BROWSER_CHROMEOS_INPUT_METHOD_XKEYBOARD_DATA_H_
204 def SplitBehavior(behavior
):
205 """Splits the behavior to compose a message or i18n-content value.
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.
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'
229 message_desc
= '%s (%s).' % (message_desc
, description
)
235 def Toi18nContent(behavior
):
236 """Composes a i18n-content value for HTML/JavaScript files.
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:]
250 """Converts the action value to shortcut keys used from JavaScript.
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
):
265 return '<>'.join(keycode
+ modifiers
)
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.'
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.'
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
)
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()
321 def PrintDiffs(message
, lhs
, rhs
):
322 """Prints the differences between |lhs| and |rhs|."""
323 dif
= set(lhs
).difference(rhs
)
325 print message
, ', '.join(dif
)
328 def FetchSpreadsheetFeeds(client
, key
, sheets
, cols
):
329 """Fetch feeds from the spreadsheet.
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
341 for entry
in worksheets_feed
.entry
:
342 worksheet_id
= entry
.id.text
.split('/')[-1]
343 list_feed
= client
.GetListFeed(key
, worksheet_id
)
345 # Hack to deal with sheet names like 'sv (Copy of fl)'
346 title
= list_feed
.title
.text
.split('(')[0].strip()
348 if title
not in sheets
:
350 print 'Reading data from the sheet: %s' % list_feed
.title
.text
351 for i
, entry
in enumerate(list_feed
.entry
):
353 for k
in entry
.custom
:
354 if (k
not in cols
) or (not entry
.custom
[k
].text
):
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
)
372 for lang
in keyboard_glyph_data
:
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']
384 line
['label'] = LABEL_MAP
.get(line
['label'], line
['label'])
385 keys
[scancode
] = line
386 # Add a label to space key
388 keys
['39'] = {'label': 'space'}
389 ret
[lang
]['keys'] = keys
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
,
400 for layout_name
, layout
in layouts
.items():
401 ret
[layout_name
[0]] = []
409 if col
!= 'scancode':
412 ret
[layout_name
[0]].append(line
)
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
)
427 for line
in hotkey_data
['Cross Platform Behaviors']:
428 if (not line
.get('chromeos')) or (line
.get('kind') != 'Key'):
430 action
= ToKeys(line
['actionctrlctrlcmdonmac'])
433 behavior
= line
['behavior'].strip()
434 description
= line
.get('descriptionfortranslation')
435 result
.append((behavior
, action
, description
))
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
)
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
459 def OutputFile(outpath
, snippet
):
460 """Output the snippet into the specified path."""
461 out
= file(outpath
, 'w')
462 out
.write(GenerateCopyrightHeader() + '\n')
464 print 'Output ' + os
.path
.normpath(outpath
)
467 def RewriteFile(start
, end
, original_dir
, original_filename
, snippet
,
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()
475 outpath
= os
.path
.join(outdir
, original_filename
)
477 outpath
= original_path
478 out
= file(outpath
, 'w')
479 rx
= re
.compile(r
'%s\n.*?%s\n' % (re
.escape(start
), re
.escape(end
)),
481 new_content
= re
.sub(rx
, '%s\n%s%s\n' % (start
, snippet
, end
),
483 out
.write(new_content
)
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."""
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
,
497 'inputMethodIdToOverlayId': INPUT_METHOD_ID_TO_OVERLAY_ID
}
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
),
517 RewriteFile(GRD_START
, GRD_END
, GRD_OUTDIR
, GRD_FILENAME
, snippet
.getvalue(),
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
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(),
536 def OutputAltGr(keyboard_glyph_data
, outdir
):
537 """Outputs the keyboard overlay data as a JSON file."""
539 caps_lock_output
= []
541 for input_method_id
, layout
in INPUT_METHOD_ID_TO_OVERLAY_ID
.iteritems():
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
)
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
)
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
)
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
)
576 layouts
= FetchLayoutsData(client
)
577 OutputJson(keyboard_glyph_data
, hotkey_data
, layouts
, 'keyboardOverlayData',
580 OutputGrd(hotkey_data
, options
.outdir
)
582 OutputCC(hotkey_data
, options
.outdir
)
584 OutputAltGr(keyboard_glyph_data
, options
.outdir
)
587 if __name__
== '__main__':