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.
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.
21 * Sent when the {@code onReset} IME event fires or uncommitted text is
22 * committed without being triggered by ChromeVox (e.g. because of a
24 * {type: 'brailleDots', dots: number}
25 * Sent when the user typed a braille cell using the standard keyboard.
26 * ChromeVox treats this similarly to entering braille input using the
28 * {type: 'backspace', requestId: string}
29 * Sent when the user presses the backspace key.
30 * ChromeVox must respond with a {@code keyEventHandled} message
31 * with the same request id.
33 * Sent from ChromeVox to this IME:
34 * {type: 'replaceText', contextID: number, deleteBefore: number,
36 * Deletes {@code deleteBefore} characters before the cursor (or selection)
37 * and inserts {@code newText}. {@code contextID} identifies the text field
38 * to apply the update to (no change will happen if focus has moved to a
40 * {type: 'setUncommitted', contextID: number, text: string}
41 * Stores text for the field identified by contextID to be committed
42 * either as a result of a 'commitUncommitted' message or a by the IME
43 * unhandled key press event. Unlike 'replaceText', this does not send the
44 * uncommitted text to the input field, but instead stores it in the IME.
45 * {type: 'commitUncommitted', contextID: number}
46 * Commits any uncommitted text if it matches the given context ID.
47 * See 'setUncommitted' above.
48 * {type: 'keyEventHandled', requestId: string, result: boolean}
49 * Response to a {@code backspace} message indicating whether the
50 * backspace was handled by ChromeVox or should be allowed to propagate
51 * through the normal event handling pipeline.
57 var BrailleIme = function() {};
59 BrailleIme
.prototype = {
61 * Whether to enable extra debug logging for the IME.
68 * ChromeVox extension ID.
72 CHROMEVOX_EXTENSION_ID_
: 'mndnfokpggljbaajbnioimlmbfngpief',
75 * Name of the port used for communication with ChromeVox.
79 PORT_NAME
: 'cvox.BrailleIme.Port',
82 * Identifier for the use standard keyboard option used in the menu and
83 * {@code localStorage}. This can be switched on to type braille using the
84 * standard keyboard, or off (default) for the usual keyboard behaviour.
87 USE_STANDARD_KEYBOARD_ID
: 'useStandardKeyboard',
89 // State related to the support for typing braille using a standrad
92 /** @private {boolean} */
93 useStandardKeyboard_
: false,
96 * Braille dots for keys that are currently pressed.
102 * Dots that have been pressed at some point since {@code pressed_} was last
109 * Bit in {@code pressed_} and {@code accumulated_} that represent
116 * Maps key codes on a standard keyboard to the correspodning dots.
117 * Keys on the 'home row' correspond to the keys on a Perkins-style keyboard.
118 * Note that the mapping below is arranged like the dots in a braille cell.
119 * Only 6 dot input is supported.
121 * @const {Object<number>}
123 CODE_TO_DOT_
: {'KeyF': 0x01, 'KeyJ': 0x08,
124 'KeyD': 0x02, 'KeyK': 0x10,
125 'KeyS': 0x04, 'KeyL': 0x20,
129 * The current engine ID as set by {@code onActivate}, or the empty string if
130 * the IME is not active.
137 * The port used to communicate with ChromeVox.
144 * Uncommitted text and context ID.
145 * @type {?{contextID: number, text: string}}
151 * Registers event listeners in the chrome IME API.
154 chrome
.input
.ime
.onActivate
.addListener(this.onActivate_
.bind(this));
155 chrome
.input
.ime
.onDeactivated
.addListener(this.onDeactivated_
.bind(this));
156 chrome
.input
.ime
.onFocus
.addListener(this.onFocus_
.bind(this));
157 chrome
.input
.ime
.onBlur
.addListener(this.onBlur_
.bind(this));
158 chrome
.input
.ime
.onInputContextUpdate
.addListener(
159 this.onInputContextUpdate_
.bind(this));
160 chrome
.input
.ime
.onKeyEvent
.addListener(this.onKeyEvent_
.bind(this),
162 chrome
.input
.ime
.onReset
.addListener(this.onReset_
.bind(this));
163 chrome
.input
.ime
.onMenuItemActivated
.addListener(
164 this.onMenuItemActivated_
.bind(this));
165 this.connectChromeVox_();
169 * Called by the IME framework when this IME is activated.
170 * @param {string} engineID Engine ID, should be 'braille'.
173 onActivate_: function(engineID
) {
174 this.log_('onActivate', engineID
);
175 this.engineID_
= engineID
;
177 this.connectChromeVox_();
179 this.useStandardKeyboard_
=
180 localStorage
[this.USE_STANDARD_KEYBOARD_ID
] === String(true);
181 this.accumulated_
= 0;
183 this.updateMenuItems_();
184 this.sendActiveState_();
188 * Called by the IME framework when this IME is deactivated.
189 * @param {string} engineID Engine ID, should be 'braille'.
192 onDeactivated_: function(engineID
) {
193 this.log_('onDectivated', engineID
);
195 this.sendActiveState_();
199 * Called by the IME framework when a text field receives focus.
200 * @param {InputContext} context Input field context.
203 onFocus_: function(context
) {
204 this.log_('onFocus', context
);
205 this.sendInputContext_(context
);
209 * Called by the IME framework when a text field looses focus.
210 * @param {number} contextID Input field context ID.
213 onBlur_: function(contextID
) {
214 this.log_('onBlur', contextID
+ '');
215 this.sendInputContext_(null);
219 * Called by the IME framework when the current input context is updated.
220 * @param {InputContext} context Input field context.
223 onInputContextUpdate_: function(context
) {
224 this.log_('onInputContextUpdate', context
);
225 this.sendInputContext_(context
);
229 * Called by the system when this IME is active and a key event is generated.
230 * @param {string} engineID Engine ID, should be 'braille'.
231 * @param {!ChromeKeyboardEvent} event The keyboard event.
234 onKeyEvent_: function(engineID
, event
) {
235 var result
= this.processKey_(event
);
236 if (result
!== undefined)
237 this.keyEventHandled_(event
.requestId
, event
.type
, result
);
241 * Called when chrome ends the current text input session.
242 * @param {string} engineID Engine ID, should be 'braille'.
245 onReset_: function(engineID
) {
246 this.log_('onReset', engineID
);
247 this.engineID_
= engineID
;
248 this.sendToChromeVox_({type
: 'reset'});
252 * Called by the IME framework when a menu item is activated.
253 * @param {string} engineID Engine ID, should be 'braille'.
254 * @param {string} itemID Identifies the menu item.
257 onMenuItemActivated_: function(engineID
, itemID
) {
258 if (engineID
=== this.engineID_
&&
259 itemID
=== this.USE_STANDARD_KEYBOARD_ID
) {
260 this.useStandardKeyboard_
= !this.useStandardKeyboard_
;
261 localStorage
[this.USE_STANDARD_KEYBOARD_ID
] =
262 String(this.useStandardKeyboard_
);
263 if (!this.useStandardKeyboard_
) {
264 this.accumulated_
= 0;
267 this.updateMenuItems_();
272 * Outputs a log message to the console, only if {@link BrailleIme.DEBUG}
274 * @param {string} func Name of the caller.
275 * @param {Object|string=} message Message to output.
278 log_: function(func
, message
) {
280 if (typeof(message
) !== 'string') {
281 message
= JSON
.stringify(message
);
283 console
.log('BrailleIme.' + func
+ ': ' + message
);
288 * Handles a qwerty key on the home row as a braille key.
289 * @param {!ChromeKeyboardEvent} event Keyboard event.
290 * @return {boolean|undefined} Whether the event was handled, or
291 * {@code undefined} if handling was delegated to ChromeVox.
294 processKey_: function(event
) {
295 if (!this.useStandardKeyboard_
) {
298 if (event
.code
=== 'Backspace' && event
.type
=== 'keydown') {
300 this.accumulated_
= 0;
301 this.sendToChromeVox_(
302 {type
: 'backspace', requestId
: event
.requestId
});
305 var dot
= this.CODE_TO_DOT_
[event
.code
];
306 if (!dot
|| event
.altKey
|| event
.ctrlKey
|| event
.shiftKey
||
309 this.accumulated_
= 0;
312 if (event
.type
=== 'keydown') {
313 this.pressed_
|= dot
;
314 this.accumulated_
|= this.pressed_
;
316 } else if (event
.type
=== 'keyup') {
317 this.pressed_
&= ~dot
;
318 if (this.pressed_
=== 0 && this.accumulated_
!== 0) {
319 var dotsToSend
= this.accumulated_
;
320 this.accumulated_
= 0;
321 if (dotsToSend
& this.SPACE
) {
322 if (dotsToSend
!= this.SPACE
) {
323 // Can't combine space and actual dot keys.
326 // Space is sent as a blank cell.
329 this.sendToChromeVox_({type
: 'brailleDots', dots
: dotsToSend
});
337 * Connects to the ChromeVox extension for message passing.
340 connectChromeVox_: function() {
342 this.port_
.disconnect();
345 this.port_
= chrome
.runtime
.connect(
346 this.CHROMEVOX_EXTENSION_ID_
, {name
: this.PORT_NAME
});
347 this.port_
.onMessage
.addListener(
348 this.onChromeVoxMessage_
.bind(this));
349 this.port_
.onDisconnect
.addListener(
350 this.onChromeVoxDisconnect_
.bind(this));
354 * Handles a message from the ChromeVox extension.
355 * @param {*} message The message from the extension.
358 onChromeVoxMessage_: function(message
) {
359 message
= /** @type {{type: string}} */ (message
);
360 this.log_('onChromeVoxMessage', message
);
361 switch (message
.type
) {
365 * @type {{contextID: number, deleteBefore: number,
369 this.replaceText_(message
.contextID
, message
.deleteBefore
,
372 case 'keyEventHandled':
374 /** @type {{requestId: string, result: boolean}} */ (message
);
375 this.keyEventHandled_(message
.requestId
, 'keydown', message
.result
);
377 case 'setUncommitted':
379 /** @type {{contextID: number, text: string}} */ (message
);
380 this.setUncommitted_(message
.contextID
, message
.text
);
382 case 'commitUncommitted':
384 /** @type {{contextID: number}} */ (message
);
385 this.commitUncommitted_(message
.contextID
);
388 console
.error('Unknown message from ChromeVox: ' +
389 JSON
.stringify(message
));
395 * Handles a disconnect event from the ChromeVox side.
398 onChromeVoxDisconnect_: function() {
400 this.log_('onChromeVoxDisconnect', chrome
.runtime
.lastError
);
404 * Sends a message to the ChromeVox extension.
405 * @param {Object} message The message to send.
408 sendToChromeVox_: function(message
) {
410 this.port_
.postMessage(message
);
415 * Sends the given input context to ChromeVox.
416 * @param {InputContext} context Input context, or null when there's no input
420 sendInputContext_: function(context
) {
421 this.sendToChromeVox_({type
: 'inputContext', context
: context
});
425 * Sends the active state to ChromeVox.
428 sendActiveState_: function() {
429 this.sendToChromeVox_({type
: 'activeState',
430 active
: this.engineID_
.length
> 0});
434 * Replaces text in the current text field.
435 * @param {number} contextID Context for the input field to replace the
437 * @param {number} deleteBefore How many characters to delete before the
439 * @param {string} toInsert Text to insert at the cursor.
441 replaceText_: function(contextID
, deleteBefore
, toInsert
) {
442 var addText
= chrome
.input
.ime
.commitText
.bind(
443 null, {contextID
: contextID
, text
: toInsert
}, function() {});
444 if (deleteBefore
> 0) {
445 var deleteText
= chrome
.input
.ime
.deleteSurroundingText
.bind(null,
446 {engineID
: this.engineID_
, contextID
: contextID
,
447 offset
: -deleteBefore
, length
: deleteBefore
}, addText
);
448 // Make sure there's no non-zero length selection so that
449 // deleteSurroundingText works correctly.
450 chrome
.input
.ime
.deleteSurroundingText(
451 {engineID
: this.engineID_
, contextID
: contextID
,
452 offset
: 0, length
: 0}, deleteText
);
459 * Responds to an asynchronous key event, indicating whether it was handled
460 * or not. If it wasn't handled, any uncommitted text is committed
461 * before sending the response to the IME API.
462 * @param {string} requestId Key event request id.
463 * @param {string} type Type of key event being responded to.
464 * @param {boolean} response Whether the IME handled the event.
466 keyEventHandled_: function(requestId
, type
, response
) {
467 if (!response
&& type
=== 'keydown' && this.uncommitted_
) {
468 this.commitUncommitted_(this.uncommitted_
.contextID
);
469 this.sendToChromeVox_({type
: 'reset'});
471 chrome
.input
.ime
.keyEventHandled(requestId
, response
);
475 * Stores uncommitted text that will be committed on any key press or
476 * when {@code commitUncommitted_} is called.
477 * @param {number} contextID of the current field.
478 * @param {string} text to store.
480 setUncommitted_: function(contextID
, text
) {
481 this.uncommitted_
= {contextID
: contextID
, text
: text
};
485 * Commits the last set uncommitted text if it matches the given context id.
486 * @param {number} contextID
488 commitUncommitted_: function(contextID
) {
489 if (this.uncommitted_
&& contextID
=== this.uncommitted_
.contextID
)
490 chrome
.input
.ime
.commitText(this.uncommitted_
);
491 this.uncommitted_
= null;
495 * Updates the menu items for this IME.
497 updateMenuItems_: function() {
498 // TODO(plundblad): Localize when translations available.
499 chrome
.input
.ime
.setMenuItems(
500 {engineID
: this.engineID_
,
503 id
: this.USE_STANDARD_KEYBOARD_ID
,
504 label
: 'Use standard keyboard for braille',
507 checked
: this.useStandardKeyboard_
,