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