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