braille_display_manager.js revision cedac228d2dd51db4b79ea1e72c7f249408ee061
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. 4 5/** 6 * @fileoverview Puts text on a braille display. 7 * 8 */ 9 10goog.provide('cvox.BrailleDisplayManager'); 11 12goog.require('cvox.BrailleCaptionsBackground'); 13goog.require('cvox.BrailleDisplayState'); 14goog.require('cvox.ExpandingBrailleTranslator'); 15goog.require('cvox.LibLouis'); 16goog.require('cvox.NavBraille'); 17 18 19/** 20 * @constructor 21 */ 22cvox.BrailleDisplayManager = function() { 23 /** 24 * @type {cvox.ExpandingBrailleTranslator} 25 * @private 26 */ 27 this.translator_ = null; 28 /** 29 * @type {!cvox.NavBraille} 30 * @private 31 */ 32 this.content_ = new cvox.NavBraille({}); 33 /** 34 * @type {!ArrayBuffer} 35 * @private 36 */ 37 this.translatedContent_ = new ArrayBuffer(0); 38 /** 39 * @type {number} 40 * @private 41 */ 42 this.panPosition_ = 0; 43 /** 44 * @type {function(!cvox.BrailleKeyEvent, cvox.NavBraille)} 45 * @private 46 */ 47 this.commandListener_ = function() {}; 48 /** 49 * Current display state used for width calculations. This is different from 50 * realDisplayState_ if the braille captions feature is enabled and there is 51 * no hardware display connected. Otherwise, it is the same object 52 * as realDisplayState_. 53 * @type {!cvox.BrailleDisplayState} 54 * @private 55 */ 56 this.displayState_ = {available: false, textCellCount: undefined}; 57 /** 58 * State reported from the chrome api, reflecting a real hardware 59 * display. 60 * @type {!cvox.BrailleDisplayState} 61 * @private 62 */ 63 this.realDisplayState_ = this.displayState_; 64 /** 65 * @type {!Array.<number>} 66 * @private 67 */ 68 this.textToBraille_ = []; 69 /** 70 * @type {Array.<number>} 71 * @private 72 */ 73 this.brailleToText_ = []; 74 75 cvox.BrailleCaptionsBackground.init(goog.bind( 76 this.onCaptionsStateChanged_, this)); 77 if (goog.isDef(chrome.brailleDisplayPrivate)) { 78 var onDisplayStateChanged = goog.bind(this.refreshDisplayState_, this); 79 chrome.brailleDisplayPrivate.getDisplayState(onDisplayStateChanged); 80 chrome.brailleDisplayPrivate.onDisplayStateChanged.addListener( 81 onDisplayStateChanged); 82 chrome.brailleDisplayPrivate.onKeyEvent.addListener( 83 goog.bind(this.onKeyEvent_, this)); 84 } else { 85 // Get the initial captions state since we won't refresh the display 86 // state in an API callback in this case. 87 this.onCaptionsStateChanged_(); 88 } 89}; 90 91 92/** 93 * Dots representing a cursor. 94 * @const 95 * @private 96 */ 97cvox.BrailleDisplayManager.CURSOR_DOTS_ = 1 << 6 | 1 << 7; 98 99 100/** 101 * @param {!cvox.NavBraille} content to send to the braille display. 102 */ 103cvox.BrailleDisplayManager.prototype.setContent = function(content) { 104 this.translateContent_(content); 105}; 106 107 108/** 109 * Sets the command listener. When a command is invoked, the listener will be 110 * called with the BrailleKeyEvent corresponding to the command and the content 111 * that was present on the display when the command was invoked. The content 112 * is guaranteed to be identical to an object previously used as the parameter 113 * to cvox.BrailleDisplayManager.setContent, or null if no content was set. 114 * @param {function(!cvox.BrailleKeyEvent, cvox.NavBraille)} func The listener. 115 */ 116cvox.BrailleDisplayManager.prototype.setCommandListener = function(func) { 117 this.commandListener_ = func; 118}; 119 120 121/** 122 * Sets the translator to be used for the braille content and refreshes the 123 * braille display with the current content using the new translator. 124 * @param {cvox.LibLouis.Translator} defaultTranslator Translator to use by 125 * default from now on. 126 * @param {cvox.LibLouis.Translator=} opt_uncontractedTranslator Translator 127 * to use around text selection end-points. 128 */ 129cvox.BrailleDisplayManager.prototype.setTranslator = 130 function(defaultTranslator, opt_uncontractedTranslator) { 131 var hadTranslator = (this.translator_ != null); 132 if (defaultTranslator) { 133 this.translator_ = new cvox.ExpandingBrailleTranslator( 134 defaultTranslator, opt_uncontractedTranslator); 135 } else { 136 this.translator_ = null; 137 } 138 this.translateContent_(this.content_); 139 if (hadTranslator && !this.translator_) { 140 this.refresh_(); 141 } 142}; 143 144 145/** 146 * @param {!cvox.BrailleDisplayState} newState Display state reported 147 * by the extension API. 148 * @private 149 */ 150cvox.BrailleDisplayManager.prototype.refreshDisplayState_ = 151 function(newState) { 152 this.realDisplayState_ = newState; 153 if (newState.available) { 154 this.displayState_ = newState; 155 } else { 156 this.displayState_ = 157 cvox.BrailleCaptionsBackground.getVirtualDisplayState(); 158 } 159 this.panPosition_ = 0; 160 this.refresh_(); 161}; 162 163 164/** 165 * Called when the state of braille captions changes. 166 * @private 167 */ 168cvox.BrailleDisplayManager.prototype.onCaptionsStateChanged_ = function() { 169 // Force reevaluation of the display state based on our stored real 170 // hardware display state, meaning that if a real display is connected, 171 // that takes precedence over the state from the captions 'virtual' display. 172 this.refreshDisplayState_(this.realDisplayState_); 173}; 174 175 176/** @private */ 177cvox.BrailleDisplayManager.prototype.refresh_ = function() { 178 if (!this.displayState_.available) { 179 return; 180 } 181 var buf = this.translatedContent_.slice(this.panPosition_, 182 this.panPosition_ + this.displayState_.textCellCount); 183 if (this.realDisplayState_.available) { 184 chrome.brailleDisplayPrivate.writeDots(buf); 185 } 186 if (cvox.BrailleCaptionsBackground.isEnabled()) { 187 var start = this.brailleToTextPosition_(this.panPosition_); 188 var end = this.brailleToTextPosition_(this.panPosition_ + buf.byteLength); 189 cvox.BrailleCaptionsBackground.setContent( 190 this.content_.text.toString().substring(start, end), buf); 191 } 192}; 193 194 195/** 196 * @param {!cvox.NavBraille} newContent new display content 197 * @private 198 */ 199cvox.BrailleDisplayManager.prototype.translateContent_ = function(newContent) { 200 if (!this.translator_) { 201 this.content_ = newContent; 202 this.translatedContent_ = new ArrayBuffer(0); 203 this.textToBraille_.length = 0; 204 this.brailleToText_.length = 0; 205 return; 206 } 207 this.translator_.translate( 208 newContent.text, 209 goog.bind(function(cells, textToBraille, brailleToText) { 210 this.content_ = newContent; 211 var startIndex = this.content_.startIndex; 212 var endIndex = this.content_.endIndex; 213 this.panPosition_ = 0; 214 if (startIndex >= 0) { 215 var translatedStartIndex; 216 var translatedEndIndex; 217 if (startIndex >= textToBraille.length) { 218 // Allow the cells to be extended with one extra cell for 219 // a carret after the last character. 220 var extCells = new ArrayBuffer(cells.byteLength + 1); 221 var extCellsView = new Uint8Array(extCells); 222 extCellsView.set(new Uint8Array(cells)); 223 // Last byte is initialized to 0. 224 cells = extCells; 225 translatedStartIndex = cells.byteLength - 1; 226 } else { 227 translatedStartIndex = textToBraille[startIndex]; 228 } 229 if (endIndex >= textToBraille.length) { 230 // endIndex can't be past-the-end of the last cell unless 231 // startIndex is too, so we don't have to do another 232 // extension here. 233 translatedEndIndex = cells.byteLength; 234 } else { 235 translatedEndIndex = textToBraille[endIndex]; 236 } 237 this.writeCursor_(cells, translatedStartIndex, translatedEndIndex); 238 if (this.displayState_.available) { 239 var textCells = this.displayState_.textCellCount; 240 this.panPosition_ = Math.floor(translatedStartIndex / textCells) * 241 textCells; 242 } 243 } 244 this.translatedContent_ = cells; 245 this.brailleToText_ = brailleToText; 246 this.textToBraille_ = textToBraille; 247 this.refresh_(); 248 }, this)); 249}; 250 251 252/** 253 * @param {cvox.BrailleKeyEvent} event 254 * @private 255 */ 256cvox.BrailleDisplayManager.prototype.onKeyEvent_ = function(event) { 257 switch (event.command) { 258 case cvox.BrailleKeyCommand.ROUTING: 259 event.displayPosition = this.brailleToTextPosition_( 260 event.displayPosition + this.panPosition_); 261 // fall through 262 case cvox.BrailleKeyCommand.LINE_UP: 263 case cvox.BrailleKeyCommand.LINE_DOWN: 264 case cvox.BrailleKeyCommand.TOP: 265 case cvox.BrailleKeyCommand.BOTTOM: 266 this.commandListener_(event, this.content_); 267 break; 268 case cvox.BrailleKeyCommand.PAN_LEFT: 269 this.panLeft_(); 270 break; 271 case cvox.BrailleKeyCommand.PAN_RIGHT: 272 this.panRight_(); 273 break; 274 } 275}; 276 277 278/** 279 * Shift the display by one full display size and refresh the content. 280 * Sends the appropriate command if the display is already at the leftmost 281 * position. 282 * @private 283 */ 284cvox.BrailleDisplayManager.prototype.panLeft_ = function() { 285 if (this.panPosition_ <= 0) { 286 this.commandListener_({ 287 command: cvox.BrailleKeyCommand.PAN_LEFT 288 }, this.content_); 289 return; 290 } 291 this.panPosition_ = Math.max( 292 0, this.panPosition_ - this.displayState_.textCellCount); 293 this.refresh_(); 294}; 295 296 297/** 298 * Shifts the display position to the right by one full display size and 299 * refreshes the content. Sends the appropriate command if the display is 300 * already at its rightmost position. 301 * @private 302 */ 303cvox.BrailleDisplayManager.prototype.panRight_ = function() { 304 var newPosition = this.panPosition_ + this.displayState_.textCellCount; 305 if (newPosition >= this.translatedContent_.byteLength) { 306 this.commandListener_({ 307 command: cvox.BrailleKeyCommand.PAN_RIGHT 308 }, this.content_); 309 return; 310 } 311 this.panPosition_ = newPosition; 312 this.refresh_(); 313}; 314 315 316/** 317 * Writes a cursor in the specified range into translated content. 318 * @param {ArrayBuffer} buffer Buffer to add cursor to. 319 * @param {number} startIndex The start index to place the cursor. 320 * @param {number} endIndex The end index to place the cursor (exclusive). 321 * @private 322 */ 323cvox.BrailleDisplayManager.prototype.writeCursor_ = function( 324 buffer, startIndex, endIndex) { 325 if (startIndex < 0 || startIndex >= buffer.byteLength || 326 endIndex < startIndex || endIndex > buffer.byteLength) { 327 return; 328 } 329 if (startIndex == endIndex) { 330 endIndex = startIndex + 1; 331 } 332 var dataView = new DataView(buffer); 333 while (startIndex < endIndex) { 334 var value = dataView.getUint8(startIndex); 335 value |= cvox.BrailleDisplayManager.CURSOR_DOTS_; 336 dataView.setUint8(startIndex, value); 337 startIndex++; 338 } 339}; 340 341 342/** 343 * Returns the text position corresponding to an absolute braille position, 344 * that is not accounting for the current pan position. 345 * @private 346 * @param {number} braillePosition Braille position relative to the startof 347 * the translated content. 348 * @return {number} 349 */ 350cvox.BrailleDisplayManager.prototype.brailleToTextPosition_ = 351 function(braillePosition) { 352 var mapping = this.brailleToText_; 353 if (braillePosition < 0) { 354 // This shouldn't happen. 355 console.error('WARNING: Braille position < 0: ' + braillePosition); 356 return 0; 357 } else if (braillePosition >= mapping.length) { 358 // This happens when the user clicks on the right part of the display 359 // when it is not entirely filled with content. Allow addressing the 360 // position after the last character. 361 return this.content_.text.getLength(); 362 } else { 363 return mapping[braillePosition]; 364 } 365}; 366