Revert of Add button to add new FSP services to Files app. (patchset #8 id:140001...
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / braille_ime / braille_ime.js
blobdeed955617be1eea14cdad4f1a0ca00c47745f34
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 'use strict';
7 /**
8 * @fileoverview Braille hardware keyboard input method.
10 * This method is automatically enabled when a braille display is connected
11 * and ChromeVox is turned on. Most of the braille input and editing logic
12 * is located in ChromeVox where the braille translation library is available.
13 * This IME connects to ChromeVox and communicates using messages as follows:
15 * Sent from this IME to ChromeVox:
16 * {type: 'activeState', active: boolean}
17 * {type: 'inputContext', context: InputContext}
18 * Sent on focus/blur to inform ChromeVox of the type of the current field.
19 * In the latter case (blur), context is null.
20 * {type: 'reset'}
21 * Sent when the {@code onReset} IME event fires.
22 * {type: 'brailleDots', dots: number}
23 * Sent when the user typed a braille cell using the standard keyboard.
24 * ChromeVox treats this similarly to entering braille input using the
25 * braille display.
26 * {type: 'backspace', requestId: string}
27 * Sent when the user presses the backspace key.
28 * ChromeVox must respond with a {@code keyEventHandled} message
29 * with the same request id.
31 * Sent from ChromeVox to this IME:
32 * {type: 'replaceText', contextID: number, deleteBefore: number,
33 * newText: string}
34 * Deletes {@code deleteBefore} characters before the cursor (or selection)
35 * and inserts {@code newText}. {@code contextID} identifies the text field
36 * to apply the update to (no change will happen if focus has moved to a
37 * different field).
38 * {type: 'keyEventHandled', requestId: string, result: boolean}
39 * Response to a {@code backspace} message indicating whether the
40 * backspace was handled by ChromeVox or should be allowed to propagate
41 * through the normal event handling pipeline.
44 /**
45 * @constructor
47 var BrailleIme = function() {};
49 BrailleIme.prototype = {
50 /**
51 * Whether to enable extra debug logging for the IME.
52 * @const {boolean}
53 * @private
55 DEBUG: false,
57 /**
58 * ChromeVox extension ID.
59 * @const {string}
60 * @private
62 CHROMEVOX_EXTENSION_ID_: 'mndnfokpggljbaajbnioimlmbfngpief',
64 /**
65 * Name of the port used for communication with ChromeVox.
66 * @const {string}
67 * @private
69 PORT_NAME: 'cvox.BrailleIme.Port',
71 /**
72 * Identifier for the use standard keyboard option used in the menu and
73 * {@code localStorage}. This can be switched on to type braille using the
74 * standard keyboard, or off (default) for the usual keyboard behaviour.
75 * @const {string}
77 USE_STANDARD_KEYBOARD_ID: 'useStandardKeyboard',
79 // State related to the support for typing braille using a standrad
80 // (qwerty) keyboard.
82 /** @private {boolean} */
83 useStandardKeyboard_: false,
85 /**
86 * Braille dots for keys that are currently pressed.
87 * @private {number}
89 pressed_: 0,
91 /**
92 * Dots that have been pressed at some point since {@code pressed_} was last
93 * {@code 0}.
94 * @private {number}
96 accumulated_: 0,
98 /**
99 * Bit in {@code pressed_} and {@code accumulated_} that represent
100 * the space key.
101 * @const {number}
103 SPACE: 0x100,
106 * Maps key codes on a standard keyboard to the correspodning dots.
107 * Keys on the 'home row' correspond to the keys on a Perkins-style keyboard.
108 * Note that the mapping below is arranged like the dots in a braille cell.
109 * Only 6 dot input is supported.
110 * @private
111 * @const {Object<string, number>}
113 CODE_TO_DOT_: {'KeyF': 0x01, 'KeyJ': 0x08,
114 'KeyD': 0x02, 'KeyK': 0x10,
115 'KeyS': 0x04, 'KeyL': 0x20,
116 'Space': 0x100 },
119 * The current engine ID as set by {@code onActivate}, or the empty string if
120 * the IME is not active.
121 * @type {string}
122 * @private
124 engineID_: '',
127 * The port used to communicate with ChromeVox.
128 * @type {Port} port_
129 * @private
131 port_: null,
134 * Registers event listeners in the chrome IME API.
136 init: function() {
137 chrome.input.ime.onActivate.addListener(this.onActivate_.bind(this));
138 chrome.input.ime.onDeactivated.addListener(this.onDeactivated_.bind(this));
139 chrome.input.ime.onFocus.addListener(this.onFocus_.bind(this));
140 chrome.input.ime.onBlur.addListener(this.onBlur_.bind(this));
141 chrome.input.ime.onInputContextUpdate.addListener(
142 this.onInputContextUpdate_.bind(this));
143 chrome.input.ime.onKeyEvent.addListener(this.onKeyEvent_.bind(this),
144 ['async']);
145 chrome.input.ime.onReset.addListener(this.onReset_.bind(this));
146 chrome.input.ime.onMenuItemActivated.addListener(
147 this.onMenuItemActivated_.bind(this));
148 this.connectChromeVox_();
152 * Called by the IME framework when this IME is activated.
153 * @param {string} engineID Engine ID, should be 'braille'.
154 * @private
156 onActivate_: function(engineID) {
157 this.log_('onActivate', engineID);
158 this.engineID_ = engineID;
159 if (!this.port_) {
160 this.connectChromeVox_();
162 this.useStandardKeyboard_ =
163 localStorage[this.USE_STANDARD_KEYBOARD_ID] === String(true);
164 this.accumulated_ = 0;
165 this.pressed_ = 0;
166 this.updateMenuItems_();
167 this.sendActiveState_();
171 * Called by the IME framework when this IME is deactivated.
172 * @param {string} engineID Engine ID, should be 'braille'.
173 * @private
175 onDeactivated_: function(engineID) {
176 this.log_('onDectivated', engineID);
177 this.engineID_ = '';
178 this.sendActiveState_();
182 * Called by the IME framework when a text field receives focus.
183 * @param {InputContext} context Input field context.
184 * @private
186 onFocus_: function(context) {
187 this.log_('onFocus', context);
188 this.sendInputContext_(context);
192 * Called by the IME framework when a text field looses focus.
193 * @param {number} contextID Input field context ID.
194 * @private
196 onBlur_: function(contextID) {
197 this.log_('onBlur', contextID + '');
198 this.sendInputContext_(null);
202 * Called by the IME framework when the current input context is updated.
203 * @param {InputContext} context Input field context.
204 * @private
206 onInputContextUpdate_: function(context) {
207 this.log_('onInputContextUpdate', context);
208 this.sendInputContext_(context);
212 * Called by the system when this IME is active and a key event is generated.
213 * @param {string} engineID Engine ID, should be 'braille'.
214 * @param {!ChromeKeyboardEvent} event The keyboard event.
215 * @private
217 onKeyEvent_: function(engineID, event) {
218 var result = this.processKey_(event);
219 if (result !== undefined) {
220 chrome.input.ime.keyEventHandled(event.requestId, result);
225 * Called when chrome ends the current text input session.
226 * @param {string} engineID Engine ID, should be 'braille'.
227 * @private
229 onReset_: function(engineID) {
230 this.log_('onReset', engineID);
231 this.engineID_ = engineID;
232 this.sendToChromeVox_({type: 'reset'});
236 * Called by the IME framework when a menu item is activated.
237 * @param {string} engineID Engine ID, should be 'braille'.
238 * @param {string} itemID Identifies the menu item.
239 * @private
241 onMenuItemActivated_: function(engineID, itemID) {
242 if (engineID === this.engineID_ &&
243 itemID === this.USE_STANDARD_KEYBOARD_ID) {
244 this.useStandardKeyboard_ = !this.useStandardKeyboard_;
245 localStorage[this.USE_STANDARD_KEYBOARD_ID] =
246 String(this.useStandardKeyboard_);
247 if (!this.useStandardKeyboard_) {
248 this.accumulated_ = 0;
249 this.pressed_ = 0;
251 this.updateMenuItems_();
256 * Outputs a log message to the console, only if {@link BrailleIme.DEBUG}
257 * is set to true.
258 * @param {string} func Name of the caller.
259 * @param {Object|string=} message Message to output.
260 * @private
262 log_: function(func, message) {
263 if (this.DEBUG) {
264 if (typeof(message) !== 'string') {
265 message = JSON.stringify(message);
267 console.log('BrailleIme.' + func + ': ' + message);
272 * Handles a qwerty key on the home row as a braille key.
273 * @param {!ChromeKeyboardEvent} event Keyboard event.
274 * @return {boolean|undefined} Whether the event was handled, or
275 * {@code undefined} if handling was delegated to ChromeVox.
276 * @private
278 processKey_: function(event) {
279 if (!this.useStandardKeyboard_) {
280 return false;
282 if (event.code === 'Backspace' && event.type === 'keydown') {
283 this.pressed_ = 0;
284 this.accumulated_ = 0;
285 this.sendToChromeVox_(
286 {type: 'backspace', requestId: event.requestId});
287 return undefined;
289 var dot = this.CODE_TO_DOT_[event.code];
290 if (!dot || event.altKey || event.ctrlKey || event.shiftKey ||
291 event.capsLock) {
292 this.pressed_ = 0;
293 this.accumulated_ = 0;
294 return false;
296 if (event.type === 'keydown') {
297 this.pressed_ |= dot;
298 this.accumulated_ |= this.pressed_;
299 return true;
300 } else if (event.type === 'keyup') {
301 this.pressed_ &= ~dot;
302 if (this.pressed_ === 0 && this.accumulated_ !== 0) {
303 var dotsToSend = this.accumulated_;
304 this.accumulated_ = 0;
305 if (dotsToSend & this.SPACE) {
306 if (dotsToSend != this.SPACE) {
307 // Can't combine space and actual dot keys.
308 return true;
310 // Space is sent as a blank cell.
311 dotsToSend = 0;
313 this.sendToChromeVox_({type: 'brailleDots', dots: dotsToSend});
315 return true;
317 return false;
321 * Connects to the ChromeVox extension for message passing.
322 * @private
324 connectChromeVox_: function() {
325 if (this.port_) {
326 this.port_.disconnect();
327 this.port_ = null;
329 this.port_ = chrome.runtime.connect(
330 this.CHROMEVOX_EXTENSION_ID_, {name: this.PORT_NAME});
331 this.port_.onMessage.addListener(
332 this.onChromeVoxMessage_.bind(this));
333 this.port_.onDisconnect.addListener(
334 this.onChromeVoxDisconnect_.bind(this));
338 * Handles a message from the ChromeVox extension.
339 * @param {*} message The message from the extension.
340 * @private
342 onChromeVoxMessage_: function(message) {
343 message = /** @type {{type: string}} */ (message);
344 this.log_('onChromeVoxMessage', message);
345 switch (message.type) {
346 case 'replaceText':
347 message =
349 * @type {{contextID: number, deleteBefore: number,
350 * newText: string}}
352 (message);
353 this.replaceText_(message.contextID, message.deleteBefore,
354 message.newText);
355 break;
356 case 'keyEventHandled':
357 message =
358 /** @type {{requestId: string, result: boolean}} */ (message);
359 chrome.input.ime.keyEventHandled(message.requestId, message.result);
360 break;
361 default:
362 console.error('Unknown message from ChromeVox: ' +
363 JSON.stringify(message));
364 break;
369 * Handles a disconnect event from the ChromeVox side.
370 * @private
372 onChromeVoxDisconnect_: function() {
373 this.port_ = null;
374 this.log_('onChromeVoxDisconnect', chrome.runtime.lastError);
378 * Sends a message to the ChromeVox extension.
379 * @param {Object} message The message to send.
380 * @private
382 sendToChromeVox_: function(message) {
383 if (this.port_) {
384 this.port_.postMessage(message);
389 * Sends the given input context to ChromeVox.
390 * @param {InputContext} context Input context, or null when there's no input
391 * context.
392 * @private
394 sendInputContext_: function(context) {
395 this.sendToChromeVox_({type: 'inputContext', context: context});
399 * Sends the active state to ChromeVox.
400 * @private
402 sendActiveState_: function() {
403 this.sendToChromeVox_({type: 'activeState',
404 active: this.engineID_.length > 0});
408 * Replaces text in the current text field.
409 * @param {number} contextID Context for the input field to replace the
410 * text in.
411 * @param {number} deleteBefore How many characters to delete before the
412 * cursor.
413 * @param {string} toInsert Text to insert at the cursor.
415 replaceText_: function(contextID, deleteBefore, toInsert) {
416 var addText = chrome.input.ime.commitText.bind(
417 null, {contextID: contextID, text: toInsert}, function() {});
418 if (deleteBefore > 0) {
419 var deleteText = chrome.input.ime.deleteSurroundingText.bind(null,
420 {engineID: this.engineID_, contextID: contextID,
421 offset: -deleteBefore, length: deleteBefore}, addText);
422 // Make sure there's no non-zero length selection so that
423 // deleteSurroundingText works correctly.
424 chrome.input.ime.deleteSurroundingText(
425 {engineID: this.engineID_, contextID: contextID,
426 offset: 0, length: 0}, deleteText);
427 } else {
428 addText();
433 * Updates the menu items for this IME.
435 updateMenuItems_: function() {
436 // TODO(plundblad): Localize when translations available.
437 chrome.input.ime.setMenuItems(
438 {engineID: this.engineID_,
439 items: [
441 id: this.USE_STANDARD_KEYBOARD_ID,
442 label: 'Use standard keyboard for braille',
443 style: 'check',
444 visible: true,
445 checked: this.useStandardKeyboard_,
446 enabled: true