gecko-dev/accessible/jsat/Presentation.jsm

333 строки
10 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* exported Presentation */
"use strict";
ChromeUtils.import("resource://gre/modules/accessibility/Utils.jsm");
ChromeUtils.defineModuleGetter(this, "PivotContext", // jshint ignore:line
"resource://gre/modules/accessibility/Utils.jsm");
ChromeUtils.defineModuleGetter(this, "UtteranceGenerator", // jshint ignore:line
"resource://gre/modules/accessibility/OutputGenerator.jsm");
ChromeUtils.defineModuleGetter(this, "States", // jshint ignore:line
"resource://gre/modules/accessibility/Constants.jsm");
ChromeUtils.defineModuleGetter(this, "Roles", // jshint ignore:line
"resource://gre/modules/accessibility/Constants.jsm");
ChromeUtils.defineModuleGetter(this, "AndroidEvents", // jshint ignore:line
"resource://gre/modules/accessibility/Constants.jsm");
var EXPORTED_SYMBOLS = ["Presentation"]; // jshint ignore:line
const EDIT_TEXT_ROLES = new Set([
Roles.SPINBUTTON, Roles.PASSWORD_TEXT,
Roles.AUTOCOMPLETE, Roles.ENTRY, Roles.EDITCOMBOBOX]);
class AndroidPresentor {
constructor() {
this.type = "Android";
this.displayedAccessibles = new WeakMap();
}
/**
* The virtual cursor's position changed.
* @param {PivotContext} aContext the context object for the new pivot
* position.
* @param {int} aReason the reason for the pivot change.
* See nsIAccessiblePivot.
* @param {bool} aBoundaryType the boundary type for the text movement
* or NO_BOUNDARY if it was not a text movement. See nsIAccessiblePivot.
*/
pivotChanged(aPosition, aOldPosition, aStartOffset, aEndOffset, aReason, aBoundaryType) {
let context = new PivotContext(
aPosition, aOldPosition, aStartOffset, aEndOffset);
if (!context.accessible) {
return null;
}
let androidEvents = [];
const isExploreByTouch = aReason == Ci.nsIAccessiblePivot.REASON_POINT;
if (isExploreByTouch) {
// This isn't really used by TalkBack so this is a half-hearted attempt
// for now.
androidEvents.push({eventType: AndroidEvents.VIEW_HOVER_EXIT, text: []});
}
if (aPosition != aOldPosition) {
let info = this._infoFromContext(context);
let eventType = isExploreByTouch ?
AndroidEvents.VIEW_HOVER_ENTER :
AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED;
androidEvents.push({...info, eventType});
try {
context.accessibleForBounds.scrollTo(
Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE);
} catch (e) {}
}
if (aBoundaryType != Ci.nsIAccessiblePivot.NO_BOUNDARY) {
const adjustedText = context.textAndAdjustedOffsets;
androidEvents.push({
eventType: AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
text: [adjustedText.text],
fromIndex: adjustedText.startOffset,
toIndex: adjustedText.endOffset
});
aPosition.QueryInterface(Ci.nsIAccessibleText).scrollSubstringTo(
aStartOffset, aEndOffset,
Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE);
}
if (context.accessible) {
this.displayedAccessibles.set(context.accessible.document.window, context);
}
return androidEvents;
}
focused(aObject) {
let info = this._infoFromContext(
new PivotContext(aObject, null, -1, -1, true, false));
return [{ eventType: AndroidEvents.VIEW_FOCUSED, ...info }];
}
/**
* An object's check action has been invoked.
* Note: Checkable objects use TalkBack's text derived from the event state, so we don't
* populate the text here.
* @param {nsIAccessible} aAccessible the object that has been invoked.
*/
checked(aAccessible) {
return [{
eventType: AndroidEvents.VIEW_CLICKED,
checked: Utils.getState(aAccessible).contains(States.CHECKED)
}];
}
/**
* An object's select action has been invoked.
* @param {nsIAccessible} aAccessible the object that has been invoked.
*/
selected(aAccessible) {
return [{
eventType: AndroidEvents.VIEW_SELECTED,
selected: Utils.getState(aAccessible).contains(States.SELECTED)
}];
}
/**
* An object's action has been invoked.
*/
actionInvoked() {
return [{ eventType: AndroidEvents.VIEW_CLICKED }];
}
/**
* Text has changed, either by the user or by the system. TODO.
*/
textChanged(aAccessible, aIsInserted, aStart, aLength, aText, aModifiedText) {
let androidEvent = {
eventType: AndroidEvents.VIEW_TEXT_CHANGED,
text: [aText],
fromIndex: aStart,
removedCount: 0,
addedCount: 0
};
if (aIsInserted) {
androidEvent.addedCount = aLength;
androidEvent.beforeText =
aText.substring(0, aStart) + aText.substring(aStart + aLength);
} else {
androidEvent.removedCount = aLength;
androidEvent.beforeText =
aText.substring(0, aStart) + aModifiedText + aText.substring(aStart);
}
return [androidEvent];
}
/**
* Text selection has changed. TODO.
*/
textSelectionChanged(aText, aStart, aEnd, aOldStart, aOldEnd, aIsFromUserInput) {
let androidEvents = [];
if (aIsFromUserInput) {
let [from, to] = aOldStart < aStart ?
[aOldStart, aStart] : [aStart, aOldStart];
androidEvents.push({
eventType: AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
text: [aText],
fromIndex: from,
toIndex: to
});
} else {
androidEvents.push({
eventType: AndroidEvents.VIEW_TEXT_SELECTION_CHANGED,
text: [aText],
fromIndex: aStart,
toIndex: aEnd,
itemCount: aText.length
});
}
return androidEvents;
}
/**
* Selection has changed.
* XXX: Implement android event?
* @param {nsIAccessible} aObject the object that has been selected.
*/
selectionChanged(aObject) {
return ["todo.selection-changed"];
}
/**
* Name has changed.
* XXX: Implement android event?
* @param {nsIAccessible} aAccessible the object whose value has changed.
*/
nameChanged(aAccessible) {
return ["todo.name-changed"];
}
/**
* Value has changed.
* XXX: Implement android event?
* @param {nsIAccessible} aAccessible the object whose value has changed.
*/
valueChanged(aAccessible) {
return ["todo.value-changed"];
}
/**
* The tab, or the tab's document state has changed.
* @param {nsIAccessible} aDocObj the tab document accessible that has had its
* state changed, or null if the tab has no associated document yet.
* @param {string} aPageState the state name for the tab, valid states are:
* 'newtab', 'loading', 'newdoc', 'loaded', 'stopped', and 'reload'.
*/
tabStateChanged(aDocObj, aPageState) {
return this.announce(
UtteranceGenerator.genForTabStateChange(aDocObj, aPageState));
}
/**
* The viewport has changed because of scroll.
* @param {Window} aWindow window of viewport that changed.
*/
viewportScrolled(aWindow) {
const { windowUtils, devicePixelRatio } = aWindow;
const resolution = { value: 1 };
windowUtils.getResolution(resolution);
const scale = devicePixelRatio * resolution.value;
return [{
eventType: AndroidEvents.VIEW_SCROLLED,
scrollX: aWindow.scrollX * scale,
scrollY: aWindow.scrollY * scale,
maxScrollX: aWindow.scrollMaxX * scale,
maxScrollY: aWindow.scrollMaxY * scale,
}];
}
/**
* The viewport has changed, either a pan, zoom, or landscape/portrait toggle.
* @param {Window} aWindow window of viewport that changed.
*/
viewportChanged(aWindow) {
const currentContext = this.displayedAccessibles.get(aWindow);
if (!currentContext) {
return;
}
const currentAcc = currentContext.accessibleForBounds;
if (Utils.isAliveAndVisible(currentAcc)) {
return [{
eventType: AndroidEvents.WINDOW_CONTENT_CHANGED,
bounds: Utils.getBounds(currentAcc)
}];
}
}
/**
* Announce something. Typically an app state change.
*/
announce(aAnnouncement) {
let localizedAnnouncement = Utils.localize(aAnnouncement).join(" ");
return [{
eventType: AndroidEvents.ANNOUNCEMENT,
text: [localizedAnnouncement],
addedCount: localizedAnnouncement.length,
removedCount: 0,
fromIndex: 0
}];
}
/**
* User tried to move cursor forward or backward with no success.
* @param {string} aMoveMethod move method that was used (eg. 'moveNext').
*/
noMove(aMoveMethod) {
return [{
eventType: AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED,
exitView: aMoveMethod,
text: [""]
}];
}
/**
* Announce a live region.
* @param {PivotContext} aContext context object for an accessible.
* @param {boolean} aIsPolite A politeness level for a live region.
* @param {boolean} aIsHide An indicator of hide/remove event.
* @param {string} aModifiedText Optional modified text.
*/
liveRegion(aAccessible, aIsPolite, aIsHide, aModifiedText) {
let context = !aModifiedText ?
new PivotContext(aAccessible, null, -1, -1, true, !!aIsHide) : null;
return this.announce(
UtteranceGenerator.genForLiveRegion(context, aIsHide, aModifiedText));
}
_infoFromContext(aContext) {
const state = Utils.getState(aContext.accessible);
const info = {
bounds: aContext.bounds,
focusable: state.contains(States.FOCUSABLE),
focused: state.contains(States.FOCUSED),
clickable: aContext.accessible.actionCount > 0,
checkable: state.contains(States.CHECKABLE),
checked: state.contains(States.CHECKED),
editable: state.contains(States.EDITABLE),
selected: state.contains(States.SELECTED)
};
if (EDIT_TEXT_ROLES.has(aContext.accessible.role)) {
let textAcc = aContext.accessible.QueryInterface(Ci.nsIAccessibleText);
return {
...info,
className: "android.widget.EditText",
hint: aContext.accessible.name,
text: [textAcc.getText(0, -1)]
};
}
return {
...info,
className: "android.view.View",
text: Utils.localize(UtteranceGenerator.genForContext(aContext)),
};
}
}
const Presentation = new AndroidPresentor();