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.
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
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,
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
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.
47 var BrailleIme = function() {};
49 BrailleIme
.prototype = {
51 * Whether to enable extra debug logging for the IME.
58 * ChromeVox extension ID.
62 CHROMEVOX_EXTENSION_ID_
: 'mndnfokpggljbaajbnioimlmbfngpief',
65 * Name of the port used for communication with ChromeVox.
69 PORT_NAME
: 'cvox.BrailleIme.Port',
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.
77 USE_STANDARD_KEYBOARD_ID
: 'useStandardKeyboard',
79 // State related to the support for typing braille using a standrad
82 /** @private {boolean} */
83 useStandardKeyboard_
: false,
86 * Braille dots for keys that are currently pressed.
92 * Dots that have been pressed at some point since {@code pressed_} was last
99 * Bit in {@code pressed_} and {@code accumulated_} that represent
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.
111 * @const {Object<string, number>}
113 CODE_TO_DOT_
: {'KeyF': 0x01, 'KeyJ': 0x08,
114 'KeyD': 0x02, 'KeyK': 0x10,
115 'KeyS': 0x04, 'KeyL': 0x20,
119 * The current engine ID as set by {@code onActivate}, or the empty string if
120 * the IME is not active.
127 * The port used to communicate with ChromeVox.
134 * Registers event listeners in the chrome IME API.
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),
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'.
156 onActivate_: function(engineID
) {
157 this.log_('onActivate', engineID
);
158 this.engineID_
= engineID
;
160 this.connectChromeVox_();
162 this.useStandardKeyboard_
=
163 localStorage
[this.USE_STANDARD_KEYBOARD_ID
] === String(true);
164 this.accumulated_
= 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'.
175 onDeactivated_: function(engineID
) {
176 this.log_('onDectivated', engineID
);
178 this.sendActiveState_();
182 * Called by the IME framework when a text field receives focus.
183 * @param {InputContext} context Input field context.
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.
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.
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.
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'.
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.
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;
251 this.updateMenuItems_();
256 * Outputs a log message to the console, only if {@link BrailleIme.DEBUG}
258 * @param {string} func Name of the caller.
259 * @param {Object|string=} message Message to output.
262 log_: function(func
, message
) {
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.
278 processKey_: function(event
) {
279 if (!this.useStandardKeyboard_
) {
282 if (event
.code
=== 'Backspace' && event
.type
=== 'keydown') {
284 this.accumulated_
= 0;
285 this.sendToChromeVox_(
286 {type
: 'backspace', requestId
: event
.requestId
});
289 var dot
= this.CODE_TO_DOT_
[event
.code
];
290 if (!dot
|| event
.altKey
|| event
.ctrlKey
|| event
.shiftKey
||
293 this.accumulated_
= 0;
296 if (event
.type
=== 'keydown') {
297 this.pressed_
|= dot
;
298 this.accumulated_
|= this.pressed_
;
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.
310 // Space is sent as a blank cell.
313 this.sendToChromeVox_({type
: 'brailleDots', dots
: dotsToSend
});
321 * Connects to the ChromeVox extension for message passing.
324 connectChromeVox_: function() {
326 this.port_
.disconnect();
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.
342 onChromeVoxMessage_: function(message
) {
343 message
= /** @type {{type: string}} */ (message
);
344 this.log_('onChromeVoxMessage', message
);
345 switch (message
.type
) {
349 * @type {{contextID: number, deleteBefore: number,
353 this.replaceText_(message
.contextID
, message
.deleteBefore
,
356 case 'keyEventHandled':
358 /** @type {{requestId: string, result: boolean}} */ (message
);
359 chrome
.input
.ime
.keyEventHandled(message
.requestId
, message
.result
);
362 console
.error('Unknown message from ChromeVox: ' +
363 JSON
.stringify(message
));
369 * Handles a disconnect event from the ChromeVox side.
372 onChromeVoxDisconnect_: function() {
374 this.log_('onChromeVoxDisconnect', chrome
.runtime
.lastError
);
378 * Sends a message to the ChromeVox extension.
379 * @param {Object} message The message to send.
382 sendToChromeVox_: function(message
) {
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
394 sendInputContext_: function(context
) {
395 this.sendToChromeVox_({type
: 'inputContext', context
: context
});
399 * Sends the active state to ChromeVox.
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
411 * @param {number} deleteBefore How many characters to delete before the
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
);
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_
,
441 id
: this.USE_STANDARD_KEYBOARD_ID
,
442 label
: 'Use standard keyboard for braille',
445 checked
: this.useStandardKeyboard_
,