blob: 5f2039a212dfe04518e72764919125341fd32e40 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Puts text on a braille display.
*
*/
goog.provide('cvox.BrailleDisplayManager');
goog.require('cvox.BrailleCaptionsBackground');
goog.require('cvox.BrailleDisplayState');
goog.require('cvox.ExpandingBrailleTranslator');
goog.require('cvox.LibLouis');
goog.require('cvox.NavBraille');
goog.require('cvox.PanStrategy');
/**
* @param {!cvox.BrailleTranslatorManager} translatorManager Keeps track
* of the current translator to use.
* @constructor
*/
cvox.BrailleDisplayManager = function(translatorManager) {
/**
* @type {!cvox.BrailleTranslatorManager}
* @private
*/
this.translatorManager_ = translatorManager;
/**
* @type {!cvox.NavBraille}
* @private
*/
this.content_ = new cvox.NavBraille({});
/**
* @type {!cvox.ExpandingBrailleTranslator.ExpansionType} valueExpansion
* @private
*/
this.expansionType_ =
cvox.ExpandingBrailleTranslator.ExpansionType.SELECTION;
/**
* @type {!ArrayBuffer}
* @private
*/
this.translatedContent_ = new ArrayBuffer(0);
/**
* @type {!ArrayBuffer}
* @private
*/
this.displayedContent_ = this.translatedContent_;
/**
* @type {cvox.PanStrategy}
* @private
*/
this.panStrategy_ = new cvox.WrappingPanStrategy();
/**
* @type {function(!cvox.BrailleKeyEvent, !cvox.NavBraille)}
* @private
*/
this.commandListener_ = function() {};
/**
* Current display state used for width calculations. This is different from
* realDisplayState_ if the braille captions feature is enabled and there is
* no hardware display connected. Otherwise, it is the same object
* as realDisplayState_.
* @type {!cvox.BrailleDisplayState}
* @private
*/
this.displayState_ = {available: false, textCellCount: undefined};
/**
* State reported from the chrome api, reflecting a real hardware
* display.
* @type {!cvox.BrailleDisplayState}
* @private
*/
this.realDisplayState_ = this.displayState_;
/**
* @type {!Array<number>}
* @private
*/
this.textToBraille_ = [];
/**
* @type {!Array<number>}
* @private
*/
this.brailleToText_ = [];
translatorManager.addChangeListener(function() {
this.translateContent_(this.content_, this.expansionType_);
}.bind(this));
chrome.storage.onChanged.addListener(function(changes, area) {
if (area == 'local' && 'brailleWordWrap' in changes) {
this.updatePanStrategy_(changes.brailleWordWrap.newValue);
}
}.bind(this));
chrome.storage.local.get({brailleWordWrap: true}, function(items) {
this.updatePanStrategy_(items.brailleWordWrap);
}.bind(this));
cvox.BrailleCaptionsBackground.init(goog.bind(
this.onCaptionsStateChanged_, this));
if (goog.isDef(chrome.brailleDisplayPrivate)) {
var onDisplayStateChanged = goog.bind(this.refreshDisplayState_, this);
chrome.brailleDisplayPrivate.getDisplayState(onDisplayStateChanged);
chrome.brailleDisplayPrivate.onDisplayStateChanged.addListener(
onDisplayStateChanged);
chrome.brailleDisplayPrivate.onKeyEvent.addListener(
goog.bind(this.onKeyEvent_, this));
} else {
// Get the initial captions state since we won't refresh the display
// state in an API callback in this case.
this.onCaptionsStateChanged_();
}
};
/**
* Dots representing a cursor.
* @const
* @private
*/
cvox.BrailleDisplayManager.CURSOR_DOTS_ = 1 << 6 | 1 << 7;
/**
* @param {!cvox.NavBraille} content Content to send to the braille display.
* @param {!cvox.ExpandingBrailleTranslator.ExpansionType} expansionType
* If the text has a {@code ValueSpan}, this indicates how that part
* of the display content is expanded when translating to braille.
* (See {@code cvox.ExpandingBrailleTranslator}).
*/
cvox.BrailleDisplayManager.prototype.setContent = function(
content, expansionType) {
this.translateContent_(content, expansionType);
};
/**
* Sets the command listener. When a command is invoked, the listener will be
* called with the BrailleKeyEvent corresponding to the command and the content
* that was present on the display when the command was invoked. The content
* is guaranteed to be identical to an object previously used as the parameter
* to cvox.BrailleDisplayManager.setContent, or null if no content was set.
* @param {function(!cvox.BrailleKeyEvent, !cvox.NavBraille)} func The listener.
*/
cvox.BrailleDisplayManager.prototype.setCommandListener = function(func) {
this.commandListener_ = func;
};
/**
* @param {!cvox.BrailleDisplayState} newState Display state reported
* by the extension API.
* @private
*/
cvox.BrailleDisplayManager.prototype.refreshDisplayState_ = function(
newState) {
var oldSize = this.displayState_.textCellCount || 0;
this.realDisplayState_ = newState;
if (newState.available) {
this.displayState_ = newState;
} else {
this.displayState_ =
cvox.BrailleCaptionsBackground.getVirtualDisplayState();
}
var newSize = this.displayState_.textCellCount || 0;
if (oldSize != newSize) {
this.panStrategy_.setDisplaySize(newSize);
}
this.refresh_();
};
/**
* Called when the state of braille captions changes.
* @private
*/
cvox.BrailleDisplayManager.prototype.onCaptionsStateChanged_ = function() {
// Force reevaluation of the display state based on our stored real
// hardware display state, meaning that if a real display is connected,
// that takes precedence over the state from the captions 'virtual' display.
this.refreshDisplayState_(this.realDisplayState_);
};
/** @private */
cvox.BrailleDisplayManager.prototype.refresh_ = function() {
if (!this.displayState_.available) {
return;
}
var viewPort = this.panStrategy_.viewPort;
var buf = this.displayedContent_.slice(viewPort.start, viewPort.end);
if (this.realDisplayState_.available) {
chrome.brailleDisplayPrivate.writeDots(buf);
}
if (cvox.BrailleCaptionsBackground.isEnabled()) {
var start = this.brailleToTextPosition_(viewPort.start);
var end = this.brailleToTextPosition_(viewPort.end);
cvox.BrailleCaptionsBackground.setContent(
this.content_.text.toString().substring(start, end), buf);
}
};
/**
* @param {!cvox.NavBraille} newContent New display content.
* @param {cvox.ExpandingBrailleTranslator.ExpansionType} newExpansionType
* How the value part of of the new content should be expanded
* with regards to contractions.
* @private
*/
cvox.BrailleDisplayManager.prototype.translateContent_ = function(
newContent, newExpansionType) {
var writeTranslatedContent = function(cells, textToBraille, brailleToText) {
this.content_ = newContent;
this.expansionType_ = newExpansionType;
this.textToBraille_ = textToBraille;
this.brailleToText_ = brailleToText;
var startIndex = this.content_.startIndex;
var endIndex = this.content_.endIndex;
var targetPosition;
if (startIndex >= 0) {
var translatedStartIndex;
var translatedEndIndex;
if (startIndex >= textToBraille.length) {
// Allow the cells to be extended with one extra cell for
// a carret after the last character.
var extCells = new ArrayBuffer(cells.byteLength + 1);
new Uint8Array(extCells).set(new Uint8Array(cells));
// Last byte is initialized to 0.
cells = extCells;
translatedStartIndex = cells.byteLength - 1;
} else {
translatedStartIndex = textToBraille[startIndex];
}
if (endIndex >= textToBraille.length) {
// endIndex can't be past-the-end of the last cell unless
// startIndex is too, so we don't have to do another
// extension here.
translatedEndIndex = cells.byteLength;
} else {
translatedEndIndex = textToBraille[endIndex];
}
this.translatedContent_ = cells;
// Copy the transalted content to a separate buffer and add the cursor
// to it.
this.displayedContent_ = new ArrayBuffer(cells.byteLength);
new Uint8Array(this.displayedContent_).set(new Uint8Array(cells));
this.writeCursor_(this.displayedContent_,
translatedStartIndex, translatedEndIndex);
targetPosition = translatedStartIndex;
} else {
this.translatedContent_ = this.displayedContent_ = cells;
targetPosition = 0;
}
this.panStrategy_.setContent(this.translatedContent_, targetPosition);
this.refresh_();
}.bind(this);
var translator = this.translatorManager_.getExpandingTranslator();
if (!translator) {
writeTranslatedContent(new ArrayBuffer(0), [], []);
} else {
translator.translate(
newContent.text,
newExpansionType,
writeTranslatedContent);
}
};
/**
* @param {cvox.BrailleKeyEvent} event The key event.
* @private
*/
cvox.BrailleDisplayManager.prototype.onKeyEvent_ = function(event) {
switch (event.command) {
case cvox.BrailleKeyCommand.PAN_LEFT:
this.panLeft_();
break;
case cvox.BrailleKeyCommand.PAN_RIGHT:
this.panRight_();
break;
case cvox.BrailleKeyCommand.ROUTING:
event.displayPosition = this.brailleToTextPosition_(
event.displayPosition + this.panStrategy_.viewPort.start);
// fall through
default:
this.commandListener_(event, this.content_);
break;
}
};
/**
* Shift the display by one full display size and refresh the content.
* Sends the appropriate command if the display is already at the leftmost
* position.
* @private
*/
cvox.BrailleDisplayManager.prototype.panLeft_ = function() {
if (this.panStrategy_.previous()) {
this.refresh_();
} else {
this.commandListener_({
command: cvox.BrailleKeyCommand.PAN_LEFT
}, this.content_);
}
};
/**
* Shifts the display position to the right by one full display size and
* refreshes the content. Sends the appropriate command if the display is
* already at its rightmost position.
* @private
*/
cvox.BrailleDisplayManager.prototype.panRight_ = function() {
if (this.panStrategy_.next()) {
this.refresh_();
} else {
this.commandListener_({
command: cvox.BrailleKeyCommand.PAN_RIGHT
}, this.content_);
}
};
/**
* Writes a cursor in the specified range into translated content.
* @param {ArrayBuffer} buffer Buffer to add cursor to.
* @param {number} startIndex The start index to place the cursor.
* @param {number} endIndex The end index to place the cursor (exclusive).
* @private
*/
cvox.BrailleDisplayManager.prototype.writeCursor_ = function(
buffer, startIndex, endIndex) {
if (startIndex < 0 || startIndex >= buffer.byteLength ||
endIndex < startIndex || endIndex > buffer.byteLength) {
return;
}
if (startIndex == endIndex) {
endIndex = startIndex + 1;
}
var dataView = new DataView(buffer);
while (startIndex < endIndex) {
var value = dataView.getUint8(startIndex);
value |= cvox.BrailleDisplayManager.CURSOR_DOTS_;
dataView.setUint8(startIndex, value);
startIndex++;
}
};
/**
* Returns the text position corresponding to an absolute braille position,
* that is not accounting for the current pan position.
* @private
* @param {number} braillePosition Braille position relative to the startof
* the translated content.
* @return {number} The mapped position in code units.
*/
cvox.BrailleDisplayManager.prototype.brailleToTextPosition_ =
function(braillePosition) {
var mapping = this.brailleToText_;
if (braillePosition < 0) {
// This shouldn't happen.
console.error('WARNING: Braille position < 0: ' + braillePosition);
return 0;
} else if (braillePosition >= mapping.length) {
// This happens when the user clicks on the right part of the display
// when it is not entirely filled with content. Allow addressing the
// position after the last character.
return this.content_.text.length;
} else {
return mapping[braillePosition];
}
};
/**
* @param {boolean} wordWrap
* @private
*/
cvox.BrailleDisplayManager.prototype.updatePanStrategy_ = function(wordWrap) {
var newStrategy = wordWrap ? new cvox.WrappingPanStrategy() :
new cvox.FixedPanStrategy();
newStrategy.setDisplaySize(this.displayState_.textCellCount || 0);
newStrategy.setContent(this.translatedContent_,
this.panStrategy_.viewPort.start);
this.panStrategy_ = newStrategy;
this.refresh_();
};