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