mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-02-17 22:32:51 +00:00
Bug 1479037 - Remove virtual content node, js autofill, and event support 2/4. r=jchen,yzen
Disabled most jsunit tests temporarily in this patch. Will modify and bring them back up in later patches, as stuff is reimplemented. Disabled most jsat mochitests. Will have a followup for but reënabling or porting, depending on the test. Depends on D6681 Differential Revision: https://phabricator.services.mozilla.com/D6682 --HG-- extra : moz-landing-system : lando
This commit is contained in:
parent
98a32cc36b
commit
3d1cc19600
@ -31,7 +31,6 @@ const GECKOVIEW_MESSAGE = {
|
||||
};
|
||||
|
||||
const ACCESSFU_MESSAGE = {
|
||||
PRESENT: "AccessFu:Present",
|
||||
DOSCROLL: "AccessFu:DoScroll",
|
||||
};
|
||||
|
||||
@ -57,11 +56,6 @@ var AccessFu = {
|
||||
this._enabled = true;
|
||||
|
||||
ChromeUtils.import("resource://gre/modules/accessibility/Utils.jsm");
|
||||
ChromeUtils.import("resource://gre/modules/accessibility/Presentation.jsm");
|
||||
|
||||
// Check for output notification
|
||||
this._notifyOutputPref =
|
||||
new PrefCache("accessibility.accessfu.notify_output");
|
||||
|
||||
Services.obs.addObserver(this, "remote-browser-shown");
|
||||
Services.obs.addObserver(this, "inprocess-browser-shown");
|
||||
@ -92,8 +86,6 @@ var AccessFu = {
|
||||
this._detachWindow(win);
|
||||
}
|
||||
|
||||
delete this._notifyOutputPref;
|
||||
|
||||
if (this.doneCallback) {
|
||||
this.doneCallback();
|
||||
delete this.doneCallback;
|
||||
@ -108,9 +100,6 @@ var AccessFu = {
|
||||
});
|
||||
|
||||
switch (aMessage.name) {
|
||||
case ACCESSFU_MESSAGE.PRESENT:
|
||||
this._output(aMessage.json, aMessage.target);
|
||||
break;
|
||||
case ACCESSFU_MESSAGE.DOSCROLL:
|
||||
this.Input.doScroll(aMessage.json, aMessage.target);
|
||||
break;
|
||||
@ -155,38 +144,6 @@ var AccessFu = {
|
||||
}
|
||||
},
|
||||
|
||||
_output: function _output(aPresentationData, aBrowser) {
|
||||
if (!aPresentationData) {
|
||||
// Either no android events to send or a string used for testing only.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Utils.isAliveAndVisible(Utils.AccService.getAccessibleFor(aBrowser))) {
|
||||
return;
|
||||
}
|
||||
|
||||
let win = aBrowser.ownerGlobal;
|
||||
|
||||
for (let evt of aPresentationData) {
|
||||
if (typeof evt == "string") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (win.WindowEventDispatcher) {
|
||||
// desktop mochitests don't have this.
|
||||
win.WindowEventDispatcher.sendRequest({
|
||||
...evt,
|
||||
type: "GeckoView:AccessibilityEvent"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this._notifyOutputPref.value) {
|
||||
Services.obs.notifyObservers(null, "accessibility-output",
|
||||
JSON.stringify(aPresentationData));
|
||||
}
|
||||
},
|
||||
|
||||
onEvent(event, data, callback) {
|
||||
switch (event) {
|
||||
case GECKOVIEW_MESSAGE.SETTINGS:
|
||||
@ -219,12 +176,6 @@ var AccessFu = {
|
||||
case GECKOVIEW_MESSAGE.SCROLL_BACKWARD:
|
||||
this.Input.androidScroll("backward");
|
||||
break;
|
||||
case GECKOVIEW_MESSAGE.VIEW_FOCUSED:
|
||||
this._focused = data.gainFocus;
|
||||
if (this._focused) {
|
||||
this.autoMove({ forcePresent: true, noOpIfOnScreen: true });
|
||||
}
|
||||
break;
|
||||
case GECKOVIEW_MESSAGE.BY_GRANULARITY:
|
||||
this.Input.moveByGranularity(data);
|
||||
break;
|
||||
@ -281,10 +232,6 @@ var AccessFu = {
|
||||
mm.sendAsyncMessage("AccessFu:AutoMove", aOptions);
|
||||
},
|
||||
|
||||
announce: function announce(aAnnouncement) {
|
||||
this._output(Presentation.announce(aAnnouncement), Utils.getCurrentBrowser());
|
||||
},
|
||||
|
||||
// So we don't enable/disable twice
|
||||
_enabled: false,
|
||||
|
||||
|
@ -14,8 +14,6 @@ ChromeUtils.defineModuleGetter(this, "TraversalRules",
|
||||
"resource://gre/modules/accessibility/Traversal.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "TraversalHelper",
|
||||
"resource://gre/modules/accessibility/Traversal.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "Presentation",
|
||||
"resource://gre/modules/accessibility/Presentation.jsm");
|
||||
|
||||
var EXPORTED_SYMBOLS = ["ContentControl"];
|
||||
|
||||
@ -155,9 +153,6 @@ this.ContentControl.prototype = {
|
||||
// We failed to move, and the message is not from a parent, so forward
|
||||
// to it.
|
||||
this.sendToParent(aMessage);
|
||||
} else {
|
||||
this._contentScope.get().sendAsyncMessage("AccessFu:Present",
|
||||
Presentation.noMove(action));
|
||||
}
|
||||
},
|
||||
|
||||
@ -228,13 +223,6 @@ this.ContentControl.prototype = {
|
||||
node.dispatchEvent(evt);
|
||||
}
|
||||
}
|
||||
|
||||
// Action invoked will be presented on checked/selected state change.
|
||||
if (!Utils.getState(aAccessible).contains(States.CHECKABLE) &&
|
||||
!Utils.getState(aAccessible).contains(States.SELECTABLE)) {
|
||||
this._contentScope.get().sendAsyncMessage("AccessFu:Present",
|
||||
Presentation.actionInvoked());
|
||||
}
|
||||
};
|
||||
|
||||
let focusedAcc = Utils.AccService.getAccessibleFor(
|
||||
@ -242,15 +230,11 @@ this.ContentControl.prototype = {
|
||||
if (focusedAcc && this.vc.position === focusedAcc
|
||||
&& focusedAcc.role === Roles.ENTRY) {
|
||||
let accText = focusedAcc.QueryInterface(Ci.nsIAccessibleText);
|
||||
let oldOffset = accText.caretOffset;
|
||||
let newOffset = aMessage.json.offset;
|
||||
let text = accText.getText(0, accText.characterCount);
|
||||
|
||||
if (newOffset >= 0 && newOffset <= accText.characterCount) {
|
||||
accText.caretOffset = newOffset;
|
||||
}
|
||||
|
||||
this.presentCaretChange(text, oldOffset, accText.caretOffset);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -399,15 +383,6 @@ this.ContentControl.prototype = {
|
||||
}
|
||||
},
|
||||
|
||||
presentCaretChange: function cc_presentCaretChange(
|
||||
aText, aOldOffset, aNewOffset) {
|
||||
if (aOldOffset !== aNewOffset) {
|
||||
let msg = Presentation.textSelectionChanged(aText, aNewOffset, aNewOffset,
|
||||
aOldOffset, aOldOffset, true);
|
||||
this._contentScope.get().sendAsyncMessage("AccessFu:Present", msg);
|
||||
}
|
||||
},
|
||||
|
||||
getChildCursor: function cc_getChildCursor(aAccessible) {
|
||||
let acc = aAccessible || this.vc.position;
|
||||
if (Utils.isAliveAndVisible(acc) && acc.role === Roles.INTERNAL_FRAME) {
|
||||
@ -456,13 +431,12 @@ this.ContentControl.prototype = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Move cursor and/or present its location.
|
||||
* Move cursor.
|
||||
* aOptions could have any of these fields:
|
||||
* - delay: in ms, before actual move is performed. Another autoMove call
|
||||
* would cancel it. Useful if we want to wait for a possible trailing
|
||||
* focus move. Default 0.
|
||||
* - noOpIfOnScreen: if accessible is alive and visible, don't do anything.
|
||||
* - forcePresent: present cursor location, whether we move or don't.
|
||||
* - moveToFocused: if there is a focused accessible move to that. This takes
|
||||
* precedence over given anchor.
|
||||
* - moveMethod: pivot move method to use, default is 'moveNext',
|
||||
@ -475,19 +449,9 @@ this.ContentControl.prototype = {
|
||||
let acc = aAnchor;
|
||||
let rule = aOptions.onScreenOnly ?
|
||||
TraversalRules.SimpleOnScreen : TraversalRules.Simple;
|
||||
let forcePresentFunc = () => {
|
||||
if (aOptions.forcePresent) {
|
||||
this._contentScope.get().sendAsyncMessage(
|
||||
"AccessFu:Present", Presentation.pivotChanged(
|
||||
vc.position, null, vc.startOffset, vc.endOffset,
|
||||
Ci.nsIAccessiblePivot.REASON_NONE,
|
||||
Ci.nsIAccessiblePivot.NO_BOUNDARY));
|
||||
}
|
||||
};
|
||||
|
||||
if (aOptions.noOpIfOnScreen &&
|
||||
Utils.isAliveAndVisible(vc.position, true)) {
|
||||
forcePresentFunc();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -509,19 +473,14 @@ this.ContentControl.prototype = {
|
||||
moved = vc[moveMethod](rule, true);
|
||||
}
|
||||
|
||||
let sentToChild = this.sendToChild(vc, {
|
||||
this.sendToChild(vc, {
|
||||
name: "AccessFu:AutoMove",
|
||||
json: {
|
||||
moveMethod: aOptions.moveMethod,
|
||||
moveToFocused: aOptions.moveToFocused,
|
||||
noOpIfOnScreen: true,
|
||||
forcePresent: true
|
||||
}
|
||||
}, null, true);
|
||||
|
||||
if (!moved && !sentToChild) {
|
||||
forcePresentFunc();
|
||||
}
|
||||
};
|
||||
|
||||
if (aOptions.delay) {
|
||||
|
@ -10,18 +10,12 @@ ChromeUtils.defineModuleGetter(this, "Utils",
|
||||
"resource://gre/modules/accessibility/Utils.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "Logger",
|
||||
"resource://gre/modules/accessibility/Utils.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "Presentation",
|
||||
"resource://gre/modules/accessibility/Presentation.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "Roles",
|
||||
"resource://gre/modules/accessibility/Constants.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "Events",
|
||||
"resource://gre/modules/accessibility/Constants.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "States",
|
||||
"resource://gre/modules/accessibility/Constants.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "clearTimeout",
|
||||
"resource://gre/modules/Timer.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "setTimeout",
|
||||
"resource://gre/modules/Timer.jsm");
|
||||
|
||||
var EXPORTED_SYMBOLS = ["EventManager"];
|
||||
|
||||
@ -33,9 +27,6 @@ function EventManager(aContentScope) {
|
||||
this.contentScope);
|
||||
this.sendMsgFunc = this.contentScope.sendAsyncMessage.bind(
|
||||
this.contentScope);
|
||||
this.webProgress = this.contentScope.docShell.
|
||||
QueryInterface(Ci.nsIInterfaceRequestor).
|
||||
getInterface(Ci.nsIWebProgress);
|
||||
}
|
||||
|
||||
this.EventManager.prototype = {
|
||||
@ -48,16 +39,8 @@ this.EventManager.prototype = {
|
||||
|
||||
AccessibilityEventObserver.addListener(this);
|
||||
|
||||
this.webProgress.addProgressListener(this,
|
||||
(Ci.nsIWebProgress.NOTIFY_STATE_ALL |
|
||||
Ci.nsIWebProgress.NOTIFY_LOCATION));
|
||||
this.addEventListener("wheel", this, true);
|
||||
this.addEventListener("scroll", this, true);
|
||||
this.addEventListener("resize", this, true);
|
||||
this._preDialogPosition = new WeakMap();
|
||||
}
|
||||
this.present(Presentation.tabStateChanged(null, "newtab"));
|
||||
|
||||
} catch (x) {
|
||||
Logger.logException(x, "Failed to start EventManager");
|
||||
}
|
||||
@ -73,10 +56,6 @@ this.EventManager.prototype = {
|
||||
AccessibilityEventObserver.removeListener(this);
|
||||
try {
|
||||
this._preDialogPosition = new WeakMap();
|
||||
this.webProgress.removeProgressListener(this);
|
||||
this.removeEventListener("wheel", this, true);
|
||||
this.removeEventListener("scroll", this, true);
|
||||
this.removeEventListener("resize", this, true);
|
||||
} catch (x) {
|
||||
// contentScope is dead.
|
||||
} finally {
|
||||
@ -88,37 +67,6 @@ this.EventManager.prototype = {
|
||||
return this.contentScope._jsat_contentControl;
|
||||
},
|
||||
|
||||
handleEvent: function handleEvent(aEvent) {
|
||||
Logger.debug(() => {
|
||||
return ["DOMEvent", aEvent.type];
|
||||
});
|
||||
|
||||
// The target could be an element, document or window
|
||||
const win = aEvent.target.ownerGlobal;
|
||||
try {
|
||||
switch (aEvent.type) {
|
||||
case "wheel":
|
||||
{
|
||||
let delta = aEvent.deltaX || aEvent.deltaY;
|
||||
this.contentControl.autoMove(
|
||||
null,
|
||||
{ moveMethod: delta > 0 ? "moveNext" : "movePrevious",
|
||||
onScreenOnly: true, noOpIfOnScreen: true, delay: 500 });
|
||||
break;
|
||||
}
|
||||
case "scroll":
|
||||
this.present(Presentation.viewportScrolled(win));
|
||||
case "resize":
|
||||
{
|
||||
this.present(Presentation.viewportChanged(win));
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (x) {
|
||||
Logger.logException(x, "Error handling DOM event");
|
||||
}
|
||||
},
|
||||
|
||||
handleAccEvent: function handleAccEvent(aEvent) {
|
||||
Logger.debug(() => {
|
||||
return ["A11yEvent", Logger.eventToString(aEvent),
|
||||
@ -156,37 +104,11 @@ this.EventManager.prototype = {
|
||||
if (!position || !Utils.getState(position).contains(States.FOCUSED)) {
|
||||
aEvent.accessibleDocument.takeFocus();
|
||||
}
|
||||
|
||||
this.present(
|
||||
Presentation.pivotChanged(position, event.oldAccessible,
|
||||
event.newStartOffset, event.newEndOffset,
|
||||
event.reason, event.boundaryType));
|
||||
|
||||
break;
|
||||
}
|
||||
case Events.STATE_CHANGE:
|
||||
{
|
||||
const event = aEvent.QueryInterface(Ci.nsIAccessibleStateChangeEvent);
|
||||
const state = Utils.getState(event);
|
||||
if (state.contains(States.CHECKED)) {
|
||||
this.present(Presentation.checked(aEvent.accessible));
|
||||
} else if (state.contains(States.SELECTED)) {
|
||||
this.present(Presentation.selected(aEvent.accessible));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Events.NAME_CHANGE:
|
||||
{
|
||||
let acc = aEvent.accessible;
|
||||
if (acc === this.contentControl.vc.position) {
|
||||
this.present(Presentation.nameChanged(acc));
|
||||
} else {
|
||||
let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
|
||||
["text", "all"]);
|
||||
if (liveRegion) {
|
||||
this.present(Presentation.nameChanged(acc, isPolite));
|
||||
}
|
||||
}
|
||||
// XXX: Port to Android
|
||||
break;
|
||||
}
|
||||
case Events.SCROLLING_START:
|
||||
@ -194,310 +116,25 @@ this.EventManager.prototype = {
|
||||
this.contentControl.autoMove(aEvent.accessible);
|
||||
break;
|
||||
}
|
||||
case Events.TEXT_CARET_MOVED:
|
||||
{
|
||||
let acc = aEvent.accessible.QueryInterface(Ci.nsIAccessibleText);
|
||||
let caretOffset = aEvent.
|
||||
QueryInterface(Ci.nsIAccessibleCaretMoveEvent).caretOffset;
|
||||
|
||||
// We could get a caret move in an accessible that is not focused,
|
||||
// it doesn't mean we are not on any editable accessible. just not
|
||||
// on this one..
|
||||
let state = Utils.getState(acc);
|
||||
if (state.contains(States.FOCUSED) && state.contains(States.EDITABLE)) {
|
||||
let fromIndex = caretOffset;
|
||||
if (acc.selectionCount) {
|
||||
const [startSel, endSel] = Utils.getTextSelection(acc);
|
||||
fromIndex = startSel == caretOffset ? endSel : startSel;
|
||||
}
|
||||
this.present(Presentation.textSelectionChanged(
|
||||
acc.getText(0, -1), fromIndex, caretOffset, 0, 0,
|
||||
aEvent.isFromUserInput));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Events.SHOW:
|
||||
{
|
||||
this._handleShow(aEvent);
|
||||
// XXX: Port to Android
|
||||
break;
|
||||
}
|
||||
case Events.HIDE:
|
||||
{
|
||||
let evt = aEvent.QueryInterface(Ci.nsIAccessibleHideEvent);
|
||||
this._handleHide(evt);
|
||||
// XXX: Port to Android
|
||||
break;
|
||||
}
|
||||
case Events.TEXT_INSERTED:
|
||||
case Events.TEXT_REMOVED:
|
||||
{
|
||||
let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
|
||||
["text", "all"]);
|
||||
if (aEvent.isFromUserInput || liveRegion) {
|
||||
// Handle all text mutations coming from the user or if they happen
|
||||
// on a live region.
|
||||
this._handleText(aEvent, liveRegion, isPolite);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Events.FOCUS:
|
||||
{
|
||||
// Put vc where the focus is at
|
||||
let acc = aEvent.accessible;
|
||||
if (![Roles.CHROME_WINDOW,
|
||||
Roles.DOCUMENT,
|
||||
Roles.APPLICATION].includes(acc.role)) {
|
||||
this.contentControl.autoMove(acc);
|
||||
}
|
||||
|
||||
this.present(Presentation.focused(acc));
|
||||
|
||||
if (Utils.inTest) {
|
||||
this.sendMsgFunc("AccessFu:Focused");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Events.DOCUMENT_LOAD_COMPLETE:
|
||||
{
|
||||
let position = this.contentControl.vc.position;
|
||||
// Check if position is in the subtree of the DOCUMENT_LOAD_COMPLETE
|
||||
// event's dialog accessible or accessible document
|
||||
let subtreeRoot = aEvent.accessible.role === Roles.DIALOG ?
|
||||
aEvent.accessible : aEvent.accessibleDocument;
|
||||
if (aEvent.accessible === aEvent.accessibleDocument ||
|
||||
(position && Utils.isInSubtree(position, subtreeRoot))) {
|
||||
// Do not automove into the document if the virtual cursor is already
|
||||
// positioned inside it.
|
||||
break;
|
||||
}
|
||||
this._preDialogPosition.set(aEvent.accessible.DOMNode, position);
|
||||
this.contentControl.autoMove(aEvent.accessible, { delay: 500 });
|
||||
break;
|
||||
}
|
||||
case Events.TEXT_VALUE_CHANGE:
|
||||
// We handle this events in TEXT_INSERTED/TEXT_REMOVED.
|
||||
break;
|
||||
case Events.VALUE_CHANGE:
|
||||
{
|
||||
let position = this.contentControl.vc.position;
|
||||
let target = aEvent.accessible;
|
||||
if (position === target ||
|
||||
Utils.getEmbeddedControl(position) === target) {
|
||||
this.present(Presentation.valueChanged(target));
|
||||
} else {
|
||||
let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
|
||||
["text", "all"]);
|
||||
if (liveRegion) {
|
||||
this.present(Presentation.valueChanged(target, isPolite));
|
||||
}
|
||||
}
|
||||
// XXX: Port to Android
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_handleShow: function _handleShow(aEvent) {
|
||||
let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
|
||||
["additions", "all"]);
|
||||
// Only handle show if it is a relevant live region.
|
||||
if (!liveRegion) {
|
||||
return;
|
||||
}
|
||||
// Show for text is handled by the EVENT_TEXT_INSERTED handler.
|
||||
if (aEvent.accessible.role === Roles.TEXT_LEAF) {
|
||||
return;
|
||||
}
|
||||
this._dequeueLiveEvent(Events.HIDE, liveRegion);
|
||||
this.present(Presentation.liveRegion(liveRegion, isPolite, false));
|
||||
},
|
||||
|
||||
_handleHide: function _handleHide(aEvent) {
|
||||
let {liveRegion, isPolite} = this._handleLiveRegion(
|
||||
aEvent, ["removals", "all"]);
|
||||
let acc = aEvent.accessible;
|
||||
if (liveRegion) {
|
||||
// Hide for text is handled by the EVENT_TEXT_REMOVED handler.
|
||||
if (acc.role === Roles.TEXT_LEAF) {
|
||||
return;
|
||||
}
|
||||
this._queueLiveEvent(Events.HIDE, liveRegion, isPolite);
|
||||
} else {
|
||||
let vc = Utils.getVirtualCursor(this.contentScope.content.document);
|
||||
if (vc.position &&
|
||||
(Utils.getState(vc.position).contains(States.DEFUNCT) ||
|
||||
Utils.isInSubtree(vc.position, acc))) {
|
||||
let position = this._preDialogPosition.get(aEvent.accessible.DOMNode) ||
|
||||
aEvent.targetPrevSibling || aEvent.targetParent;
|
||||
if (!position) {
|
||||
try {
|
||||
position = acc.previousSibling;
|
||||
} catch (x) {
|
||||
// Accessible is unattached from the accessible tree.
|
||||
position = acc.parent;
|
||||
}
|
||||
}
|
||||
this.contentControl.autoMove(position,
|
||||
{ moveToFocused: true, delay: 500 });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_handleText: function _handleText(aEvent, aLiveRegion, aIsPolite) {
|
||||
let event = aEvent.QueryInterface(Ci.nsIAccessibleTextChangeEvent);
|
||||
let isInserted = event.isInserted;
|
||||
let txtIface = aEvent.accessible.QueryInterface(Ci.nsIAccessibleText);
|
||||
|
||||
let text = "";
|
||||
try {
|
||||
text = txtIface.getText(0, Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT);
|
||||
} catch (x) {
|
||||
// XXX we might have gotten an exception with of a
|
||||
// zero-length text. If we did, ignore it (bug #749810).
|
||||
if (txtIface.characterCount) {
|
||||
throw x;
|
||||
}
|
||||
}
|
||||
// If there are embedded objects in the text, ignore them.
|
||||
// Assuming changes to the descendants would already be handled by the
|
||||
// show/hide event.
|
||||
let modifiedText = event.modifiedText.replace(/\uFFFC/g, "");
|
||||
if (modifiedText != event.modifiedText && !modifiedText.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (aLiveRegion) {
|
||||
if (aEvent.eventType === Events.TEXT_REMOVED) {
|
||||
this._queueLiveEvent(Events.TEXT_REMOVED, aLiveRegion, aIsPolite,
|
||||
modifiedText);
|
||||
} else {
|
||||
this._dequeueLiveEvent(Events.TEXT_REMOVED, aLiveRegion);
|
||||
this.present(Presentation.liveRegion(aLiveRegion, aIsPolite, false,
|
||||
modifiedText));
|
||||
}
|
||||
} else {
|
||||
this.present(Presentation.textChanged(aEvent.accessible, isInserted,
|
||||
event.start, event.length, text, modifiedText));
|
||||
}
|
||||
},
|
||||
|
||||
_handleLiveRegion: function _handleLiveRegion(aEvent, aRelevant) {
|
||||
if (aEvent.isFromUserInput) {
|
||||
return {};
|
||||
}
|
||||
let parseLiveAttrs = function parseLiveAttrs(aAccessible) {
|
||||
let attrs = Utils.getAttributes(aAccessible);
|
||||
if (attrs["container-live"]) {
|
||||
return {
|
||||
live: attrs["container-live"],
|
||||
relevant: attrs["container-relevant"] || "additions text",
|
||||
busy: attrs["container-busy"],
|
||||
atomic: attrs["container-atomic"],
|
||||
memberOf: attrs["member-of"]
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
// XXX live attributes are not set for hidden accessibles yet. Need to
|
||||
// climb up the tree to check for them.
|
||||
let getLiveAttributes = function getLiveAttributes(aEvent) {
|
||||
let liveAttrs = parseLiveAttrs(aEvent.accessible);
|
||||
if (liveAttrs) {
|
||||
return liveAttrs;
|
||||
}
|
||||
let parent = aEvent.targetParent;
|
||||
while (parent) {
|
||||
liveAttrs = parseLiveAttrs(parent);
|
||||
if (liveAttrs) {
|
||||
return liveAttrs;
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
let {live, relevant, /* busy, atomic, memberOf */ } = getLiveAttributes(aEvent);
|
||||
// If container-live is not present or is set to |off| ignore the event.
|
||||
if (!live || live === "off") {
|
||||
return {};
|
||||
}
|
||||
// XXX: support busy and atomic.
|
||||
|
||||
// Determine if the type of the mutation is relevant. Default is additions
|
||||
// and text.
|
||||
let isRelevant = Utils.matchAttributeValue(relevant, aRelevant);
|
||||
if (!isRelevant) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
liveRegion: aEvent.accessible,
|
||||
isPolite: live === "polite"
|
||||
};
|
||||
},
|
||||
|
||||
_dequeueLiveEvent: function _dequeueLiveEvent(aEventType, aLiveRegion) {
|
||||
let domNode = aLiveRegion.DOMNode;
|
||||
if (this._liveEventQueue && this._liveEventQueue.has(domNode)) {
|
||||
let queue = this._liveEventQueue.get(domNode);
|
||||
let nextEvent = queue[0];
|
||||
if (nextEvent.eventType === aEventType) {
|
||||
clearTimeout(nextEvent.timeoutID);
|
||||
queue.shift();
|
||||
if (queue.length === 0) {
|
||||
this._liveEventQueue.delete(domNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_queueLiveEvent: function _queueLiveEvent(aEventType, aLiveRegion, aIsPolite, aModifiedText) {
|
||||
if (!this._liveEventQueue) {
|
||||
this._liveEventQueue = new WeakMap();
|
||||
}
|
||||
let eventHandler = {
|
||||
eventType: aEventType,
|
||||
timeoutID: setTimeout(this.present.bind(this),
|
||||
20, // Wait for a possible EVENT_SHOW or EVENT_TEXT_INSERTED event.
|
||||
Presentation.liveRegion(aLiveRegion, aIsPolite, true, aModifiedText))
|
||||
};
|
||||
|
||||
let domNode = aLiveRegion.DOMNode;
|
||||
if (this._liveEventQueue.has(domNode)) {
|
||||
this._liveEventQueue.get(domNode).push(eventHandler);
|
||||
} else {
|
||||
this._liveEventQueue.set(domNode, [eventHandler]);
|
||||
}
|
||||
},
|
||||
|
||||
present: function present(aPresentationData) {
|
||||
if (aPresentationData && aPresentationData.length > 0) {
|
||||
this.sendMsgFunc("AccessFu:Present", aPresentationData);
|
||||
}
|
||||
},
|
||||
|
||||
onStateChange: function onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
|
||||
let tabstate = "";
|
||||
|
||||
let loadingState = Ci.nsIWebProgressListener.STATE_TRANSFERRING |
|
||||
Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
|
||||
let loadedState = Ci.nsIWebProgressListener.STATE_STOP |
|
||||
Ci.nsIWebProgressListener.STATE_IS_NETWORK;
|
||||
|
||||
if ((aStateFlags & loadingState) == loadingState) {
|
||||
tabstate = "loading";
|
||||
} else if ((aStateFlags & loadedState) == loadedState &&
|
||||
!aWebProgress.isLoadingDocument) {
|
||||
tabstate = "loaded";
|
||||
}
|
||||
|
||||
if (tabstate) {
|
||||
let docAcc = Utils.AccService.getAccessibleFor(aWebProgress.DOMWindow.document);
|
||||
this.present(Presentation.tabStateChanged(docAcc, tabstate));
|
||||
}
|
||||
},
|
||||
|
||||
onLocationChange: function onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
|
||||
let docAcc = Utils.AccService.getAccessibleFor(aWebProgress.DOMWindow.document);
|
||||
this.present(Presentation.tabStateChanged(docAcc, "newdoc"));
|
||||
},
|
||||
|
||||
QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference, Ci.nsIObserver])
|
||||
QueryInterface: ChromeUtils.generateQI([Ci.nsISupportsWeakReference, Ci.nsIObserver])
|
||||
};
|
||||
|
||||
const AccessibilityEventObserver = {
|
||||
|
@ -1,824 +0,0 @@
|
||||
/* 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 UtteranceGenerator */
|
||||
|
||||
"use strict";
|
||||
|
||||
const INCLUDE_DESC = 0x01;
|
||||
const INCLUDE_NAME = 0x02;
|
||||
const INCLUDE_VALUE = 0x04;
|
||||
const NAME_FROM_SUBTREE_RULE = 0x10;
|
||||
const IGNORE_EXPLICIT_NAME = 0x20;
|
||||
|
||||
const OUTPUT_DESC_FIRST = 0;
|
||||
const OUTPUT_DESC_LAST = 1;
|
||||
|
||||
ChromeUtils.defineModuleGetter(this, "Utils", // jshint ignore:line
|
||||
"resource://gre/modules/accessibility/Utils.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "PrefCache", // jshint ignore:line
|
||||
"resource://gre/modules/accessibility/Utils.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "Logger", // jshint ignore:line
|
||||
"resource://gre/modules/accessibility/Utils.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "Roles", // jshint ignore:line
|
||||
"resource://gre/modules/accessibility/Constants.jsm");
|
||||
ChromeUtils.defineModuleGetter(this, "States", // jshint ignore:line
|
||||
"resource://gre/modules/accessibility/Constants.jsm");
|
||||
|
||||
var EXPORTED_SYMBOLS = ["UtteranceGenerator"]; // jshint ignore:line
|
||||
|
||||
var OutputGenerator = {
|
||||
|
||||
defaultOutputOrder: OUTPUT_DESC_LAST,
|
||||
|
||||
/**
|
||||
* Generates output for a PivotContext.
|
||||
* @param {PivotContext} aContext object that generates and caches
|
||||
* context information for a given accessible and its relationship with
|
||||
* another accessible.
|
||||
* @return {Object} An array of speech data. Depending on the utterance order,
|
||||
* the data describes the context for an accessible object either
|
||||
* starting from the accessible's ancestry or accessible's subtree.
|
||||
*/
|
||||
genForContext: function genForContext(aContext) {
|
||||
let output = [];
|
||||
let self = this;
|
||||
let addOutput = function addOutput(aAccessible) {
|
||||
output.push.apply(output, self.genForObject(aAccessible, aContext));
|
||||
};
|
||||
let ignoreSubtree = function ignoreSubtree(aAccessible) {
|
||||
let roleString = Utils.AccService.getStringRole(aAccessible.role);
|
||||
let nameRule = self.roleRuleMap[roleString] || 0;
|
||||
// Ignore subtree if the name is explicit and the role's name rule is the
|
||||
// NAME_FROM_SUBTREE_RULE.
|
||||
return (((nameRule & INCLUDE_VALUE) && aAccessible.value) ||
|
||||
((nameRule & NAME_FROM_SUBTREE_RULE) &&
|
||||
(Utils.getAttributes(aAccessible)["explicit-name"] === "true" &&
|
||||
!(nameRule & IGNORE_EXPLICIT_NAME))));
|
||||
};
|
||||
|
||||
let contextStart = this._getContextStart(aContext);
|
||||
|
||||
if (this.outputOrder === OUTPUT_DESC_FIRST) {
|
||||
contextStart.forEach(addOutput);
|
||||
addOutput(aContext.accessible);
|
||||
for (let node of aContext.subtreeGenerator(true, ignoreSubtree)) {
|
||||
addOutput(node);
|
||||
}
|
||||
} else {
|
||||
for (let node of aContext.subtreeGenerator(false, ignoreSubtree)) {
|
||||
addOutput(node);
|
||||
}
|
||||
addOutput(aContext.accessible);
|
||||
|
||||
// If there are any documents in new ancestry, find a first one and place
|
||||
// it in the beginning of the utterance.
|
||||
let doc, docIndex = contextStart.findIndex(
|
||||
ancestor => ancestor.role === Roles.DOCUMENT);
|
||||
|
||||
if (docIndex > -1) {
|
||||
doc = contextStart.splice(docIndex, 1)[0];
|
||||
}
|
||||
|
||||
contextStart.reverse().forEach(addOutput);
|
||||
if (doc) {
|
||||
output.unshift.apply(output, self.genForObject(doc, aContext));
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Generates output for an object.
|
||||
* @param {nsIAccessible} aAccessible accessible object to generate output
|
||||
* for.
|
||||
* @param {PivotContext} aContext object that generates and caches
|
||||
* context information for a given accessible and its relationship with
|
||||
* another accessible.
|
||||
* @return {Array} A 2 element array of speech data. The first element
|
||||
* describes the object and its state. The second element is the object's
|
||||
* name. Whether the object's description or it's role is included is
|
||||
* determined by {@link roleRuleMap}.
|
||||
*/
|
||||
genForObject: function genForObject(aAccessible, aContext) {
|
||||
let roleString = Utils.AccService.getStringRole(aAccessible.role);
|
||||
let func = this.objectOutputFunctions[
|
||||
OutputGenerator._getOutputName(roleString)] ||
|
||||
this.objectOutputFunctions.defaultFunc;
|
||||
|
||||
let flags = this.roleRuleMap[roleString] || 0;
|
||||
|
||||
if (aAccessible.childCount === 0) {
|
||||
flags |= INCLUDE_NAME;
|
||||
}
|
||||
|
||||
return func.apply(this, [aAccessible, roleString,
|
||||
Utils.getState(aAccessible), flags, aContext]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates output for an action performed.
|
||||
* @param {nsIAccessible} aAccessible accessible object that the action was
|
||||
* invoked in.
|
||||
* @param {string} aActionName the name of the action, one of the keys in
|
||||
* {@link gActionMap}.
|
||||
* @return {Array} A one element array with action data.
|
||||
*/
|
||||
genForAction: function genForAction(aObject, aActionName) {}, // jshint ignore:line
|
||||
|
||||
/**
|
||||
* Generates output for an announcement.
|
||||
* @param {string} aAnnouncement unlocalized announcement.
|
||||
* @return {Array} An announcement speech data to be localized.
|
||||
*/
|
||||
genForAnnouncement: function genForAnnouncement(aAnnouncement) {}, // jshint ignore:line
|
||||
|
||||
/**
|
||||
* Generates output for a tab state change.
|
||||
* @param {nsIAccessible} aAccessible accessible object of the tab's attached
|
||||
* document.
|
||||
* @param {string} aTabState the tab state name, see
|
||||
* {@link Presenter.tabStateChanged}.
|
||||
* @return {Array} The tab state utterace.
|
||||
*/
|
||||
genForTabStateChange: function genForTabStateChange(aObject, aTabState) {}, // jshint ignore:line
|
||||
|
||||
/**
|
||||
* Generates output for announcing entering and leaving editing mode.
|
||||
* @param {aIsEditing} boolean true if we are in editing mode
|
||||
* @return {Array} The mode utterance
|
||||
*/
|
||||
genForEditingMode: function genForEditingMode(aIsEditing) {}, // jshint ignore:line
|
||||
|
||||
_getContextStart: function getContextStart(aContext) {}, // jshint ignore:line
|
||||
|
||||
/**
|
||||
* Adds an accessible name and description to the output if available.
|
||||
* @param {Array} aOutput Output array.
|
||||
* @param {nsIAccessible} aAccessible current accessible object.
|
||||
* @param {Number} aFlags output flags.
|
||||
*/
|
||||
_addName: function _addName(aOutput, aAccessible, aFlags) {
|
||||
let name;
|
||||
if ((Utils.getAttributes(aAccessible)["explicit-name"] === "true" &&
|
||||
!(aFlags & IGNORE_EXPLICIT_NAME)) || (aFlags & INCLUDE_NAME)) {
|
||||
name = aAccessible.name;
|
||||
}
|
||||
|
||||
let description = aAccessible.description;
|
||||
if (description) {
|
||||
// Compare against the calculated name unconditionally, regardless of name rule,
|
||||
// so we can make sure we don't speak duplicated descriptions
|
||||
let tmpName = name || aAccessible.name;
|
||||
if (tmpName && (description !== tmpName)) {
|
||||
if (name) {
|
||||
name = this.outputOrder === OUTPUT_DESC_FIRST ?
|
||||
description + " - " + name :
|
||||
name + " - " + description;
|
||||
} else {
|
||||
name = description;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!name || !name.trim()) {
|
||||
return;
|
||||
}
|
||||
aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? "push" : "unshift"](name);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a landmark role to the output if available.
|
||||
* @param {Array} aOutput Output array.
|
||||
* @param {nsIAccessible} aAccessible current accessible object.
|
||||
*/
|
||||
_addLandmark: function _addLandmark(aOutput, aAccessible) {
|
||||
let landmarkName = Utils.getLandmarkName(aAccessible);
|
||||
if (!landmarkName) {
|
||||
return;
|
||||
}
|
||||
aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? "unshift" : "push"]({
|
||||
string: landmarkName
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds math roles to the output, for a MathML accessible.
|
||||
* @param {Array} aOutput Output array.
|
||||
* @param {nsIAccessible} aAccessible current accessible object.
|
||||
* @param {String} aRoleStr aAccessible's role string.
|
||||
*/
|
||||
_addMathRoles: function _addMathRoles(aOutput, aAccessible, aRoleStr) {
|
||||
// First, determine the actual role to use (e.g. mathmlfraction).
|
||||
let roleStr = aRoleStr;
|
||||
switch (aAccessible.role) {
|
||||
case Roles.MATHML_CELL:
|
||||
case Roles.MATHML_ENCLOSED:
|
||||
case Roles.MATHML_LABELED_ROW:
|
||||
case Roles.MATHML_ROOT:
|
||||
case Roles.MATHML_SQUARE_ROOT:
|
||||
case Roles.MATHML_TABLE:
|
||||
case Roles.MATHML_TABLE_ROW:
|
||||
// Use the default role string.
|
||||
break;
|
||||
case Roles.MATHML_MULTISCRIPTS:
|
||||
case Roles.MATHML_OVER:
|
||||
case Roles.MATHML_SUB:
|
||||
case Roles.MATHML_SUB_SUP:
|
||||
case Roles.MATHML_SUP:
|
||||
case Roles.MATHML_UNDER:
|
||||
case Roles.MATHML_UNDER_OVER:
|
||||
// For scripted accessibles, use the string 'mathmlscripted'.
|
||||
roleStr = "mathmlscripted";
|
||||
break;
|
||||
case Roles.MATHML_FRACTION:
|
||||
// From a semantic point of view, the only important point is to
|
||||
// distinguish between fractions that have a bar and those that do not.
|
||||
// Per the MathML 3 spec, the latter happens iff the linethickness
|
||||
// attribute is of the form [zero-float][optional-unit]. In that case,
|
||||
// we use the string 'mathmlfractionwithoutbar'.
|
||||
let linethickness = Utils.getAttributes(aAccessible).linethickness;
|
||||
if (linethickness) {
|
||||
let numberMatch = linethickness.match(/^(?:\d|\.)+/);
|
||||
if (numberMatch && !parseFloat(numberMatch[0])) {
|
||||
roleStr += "withoutbar";
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Otherwise, do not output the actual role.
|
||||
roleStr = null;
|
||||
break;
|
||||
}
|
||||
|
||||
// Get the math role based on the position in the parent accessible
|
||||
// (e.g. numerator for the first child of a mathmlfraction).
|
||||
let mathRole = Utils.getMathRole(aAccessible);
|
||||
if (mathRole) {
|
||||
aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? "push" : "unshift"]({
|
||||
string: this._getOutputName(mathRole)});
|
||||
}
|
||||
if (roleStr) {
|
||||
aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? "push" : "unshift"]({
|
||||
string: this._getOutputName(roleStr)});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds MathML menclose notations to the output.
|
||||
* @param {Array} aOutput Output array.
|
||||
* @param {nsIAccessible} aAccessible current accessible object.
|
||||
*/
|
||||
_addMencloseNotations: function _addMencloseNotations(aOutput, aAccessible) {
|
||||
let notations = Utils.getAttributes(aAccessible).notation || "longdiv";
|
||||
aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? "push" : "unshift"].apply(
|
||||
aOutput, notations.split(" ").map(notation => {
|
||||
return { string: this._getOutputName("notation-" + notation) };
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds an entry type attribute to the description if available.
|
||||
* @param {Array} aOutput Output array.
|
||||
* @param {nsIAccessible} aAccessible current accessible object.
|
||||
* @param {String} aRoleStr aAccessible's role string.
|
||||
*/
|
||||
_addType: function _addType(aOutput, aAccessible, aRoleStr) {
|
||||
if (aRoleStr !== "entry") {
|
||||
return;
|
||||
}
|
||||
|
||||
let typeName = Utils.getAttributes(aAccessible)["text-input-type"];
|
||||
// Ignore the the input type="text" case.
|
||||
if (!typeName || typeName === "text") {
|
||||
return;
|
||||
}
|
||||
aOutput.push({string: "textInputType_" + typeName});
|
||||
},
|
||||
|
||||
_addState: function _addState(aOutput, aState, aRoleStr) {}, // jshint ignore:line
|
||||
|
||||
_addRole: function _addRole(aOutput, aAccessible, aRoleStr) {}, // jshint ignore:line
|
||||
|
||||
get outputOrder() {
|
||||
if (!this._utteranceOrder) {
|
||||
this._utteranceOrder = new PrefCache("accessibility.accessfu.utterance");
|
||||
}
|
||||
return typeof this._utteranceOrder.value === "number" ?
|
||||
this._utteranceOrder.value : this.defaultOutputOrder;
|
||||
},
|
||||
|
||||
_getOutputName: function _getOutputName(aName) {
|
||||
return aName.replace(/\s/g, "");
|
||||
},
|
||||
|
||||
roleRuleMap: {
|
||||
"menubar": INCLUDE_DESC,
|
||||
"scrollbar": INCLUDE_DESC,
|
||||
"grip": INCLUDE_DESC,
|
||||
"alert": INCLUDE_DESC | INCLUDE_NAME,
|
||||
"menupopup": INCLUDE_DESC,
|
||||
"menuitem": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
|
||||
"tooltip": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
|
||||
"columnheader": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
|
||||
"rowheader": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
|
||||
"column": NAME_FROM_SUBTREE_RULE,
|
||||
"row": NAME_FROM_SUBTREE_RULE,
|
||||
"cell": INCLUDE_DESC | INCLUDE_NAME,
|
||||
"application": INCLUDE_NAME,
|
||||
"document": INCLUDE_NAME | NAME_FROM_SUBTREE_RULE, // don't use the subtree of entire document
|
||||
"grouping": INCLUDE_DESC | INCLUDE_NAME,
|
||||
"toolbar": INCLUDE_DESC,
|
||||
"table": INCLUDE_DESC | INCLUDE_NAME,
|
||||
"link": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
|
||||
"helpballoon": NAME_FROM_SUBTREE_RULE,
|
||||
"list": INCLUDE_DESC | INCLUDE_NAME,
|
||||
"listitem": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
|
||||
"outline": INCLUDE_DESC,
|
||||
"outlineitem": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
|
||||
"pagetab": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
|
||||
"graphic": INCLUDE_DESC,
|
||||
"switch": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
|
||||
"pushbutton": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
|
||||
"checkbutton": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
|
||||
"radiobutton": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
|
||||
"buttondropdown": NAME_FROM_SUBTREE_RULE,
|
||||
"combobox": INCLUDE_DESC | INCLUDE_VALUE,
|
||||
"droplist": INCLUDE_DESC,
|
||||
"progressbar": INCLUDE_DESC | INCLUDE_VALUE,
|
||||
"slider": INCLUDE_DESC | INCLUDE_VALUE,
|
||||
"spinbutton": INCLUDE_DESC | INCLUDE_VALUE,
|
||||
"diagram": INCLUDE_DESC,
|
||||
"animation": INCLUDE_DESC,
|
||||
"equation": INCLUDE_DESC,
|
||||
"buttonmenu": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
|
||||
"buttondropdowngrid": NAME_FROM_SUBTREE_RULE,
|
||||
"pagetablist": INCLUDE_DESC,
|
||||
"canvas": INCLUDE_DESC,
|
||||
"check menu item": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
|
||||
"label": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
|
||||
"password text": INCLUDE_DESC,
|
||||
"popup menu": INCLUDE_DESC,
|
||||
"radio menu item": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
|
||||
"table column header": NAME_FROM_SUBTREE_RULE,
|
||||
"table row header": NAME_FROM_SUBTREE_RULE,
|
||||
"tear off menu item": NAME_FROM_SUBTREE_RULE,
|
||||
"toggle button": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
|
||||
"parent menuitem": NAME_FROM_SUBTREE_RULE,
|
||||
"header": INCLUDE_DESC,
|
||||
"footer": INCLUDE_DESC,
|
||||
"entry": INCLUDE_DESC | INCLUDE_NAME | INCLUDE_VALUE,
|
||||
"caption": INCLUDE_DESC,
|
||||
"document frame": INCLUDE_DESC,
|
||||
"heading": INCLUDE_DESC,
|
||||
"calendar": INCLUDE_DESC | INCLUDE_NAME,
|
||||
"combobox option": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
|
||||
"listbox option": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
|
||||
"listbox rich option": NAME_FROM_SUBTREE_RULE,
|
||||
"gridcell": NAME_FROM_SUBTREE_RULE,
|
||||
"check rich option": NAME_FROM_SUBTREE_RULE,
|
||||
"term": NAME_FROM_SUBTREE_RULE,
|
||||
"definition": NAME_FROM_SUBTREE_RULE,
|
||||
"key": NAME_FROM_SUBTREE_RULE,
|
||||
"image map": INCLUDE_DESC,
|
||||
"option": INCLUDE_DESC,
|
||||
"listbox": INCLUDE_DESC,
|
||||
"definitionlist": INCLUDE_DESC | INCLUDE_NAME,
|
||||
"dialog": INCLUDE_DESC | INCLUDE_NAME,
|
||||
"chrome window": IGNORE_EXPLICIT_NAME,
|
||||
"app root": IGNORE_EXPLICIT_NAME,
|
||||
"statusbar": NAME_FROM_SUBTREE_RULE,
|
||||
"mathml table": INCLUDE_DESC | INCLUDE_NAME,
|
||||
"mathml labeled row": NAME_FROM_SUBTREE_RULE,
|
||||
"mathml table row": NAME_FROM_SUBTREE_RULE,
|
||||
"mathml cell": INCLUDE_DESC | INCLUDE_NAME,
|
||||
"mathml fraction": INCLUDE_DESC,
|
||||
"mathml square root": INCLUDE_DESC,
|
||||
"mathml root": INCLUDE_DESC,
|
||||
"mathml enclosed": INCLUDE_DESC,
|
||||
"mathml sub": INCLUDE_DESC,
|
||||
"mathml sup": INCLUDE_DESC,
|
||||
"mathml sub sup": INCLUDE_DESC,
|
||||
"mathml under": INCLUDE_DESC,
|
||||
"mathml over": INCLUDE_DESC,
|
||||
"mathml under over": INCLUDE_DESC,
|
||||
"mathml multiscripts": INCLUDE_DESC,
|
||||
"mathml identifier": INCLUDE_DESC,
|
||||
"mathml number": INCLUDE_DESC,
|
||||
"mathml operator": INCLUDE_DESC,
|
||||
"mathml text": INCLUDE_DESC,
|
||||
"mathml string literal": INCLUDE_DESC,
|
||||
"mathml row": INCLUDE_DESC,
|
||||
"mathml style": INCLUDE_DESC,
|
||||
"mathml error": INCLUDE_DESC },
|
||||
|
||||
mathmlRolesSet: new Set([
|
||||
Roles.MATHML_MATH,
|
||||
Roles.MATHML_IDENTIFIER,
|
||||
Roles.MATHML_NUMBER,
|
||||
Roles.MATHML_OPERATOR,
|
||||
Roles.MATHML_TEXT,
|
||||
Roles.MATHML_STRING_LITERAL,
|
||||
Roles.MATHML_GLYPH,
|
||||
Roles.MATHML_ROW,
|
||||
Roles.MATHML_FRACTION,
|
||||
Roles.MATHML_SQUARE_ROOT,
|
||||
Roles.MATHML_ROOT,
|
||||
Roles.MATHML_FENCED,
|
||||
Roles.MATHML_ENCLOSED,
|
||||
Roles.MATHML_STYLE,
|
||||
Roles.MATHML_SUB,
|
||||
Roles.MATHML_SUP,
|
||||
Roles.MATHML_SUB_SUP,
|
||||
Roles.MATHML_UNDER,
|
||||
Roles.MATHML_OVER,
|
||||
Roles.MATHML_UNDER_OVER,
|
||||
Roles.MATHML_MULTISCRIPTS,
|
||||
Roles.MATHML_TABLE,
|
||||
Roles.LABELED_ROW,
|
||||
Roles.MATHML_TABLE_ROW,
|
||||
Roles.MATHML_CELL,
|
||||
Roles.MATHML_ACTION,
|
||||
Roles.MATHML_ERROR,
|
||||
Roles.MATHML_STACK,
|
||||
Roles.MATHML_LONG_DIVISION,
|
||||
Roles.MATHML_STACK_GROUP,
|
||||
Roles.MATHML_STACK_ROW,
|
||||
Roles.MATHML_STACK_CARRIES,
|
||||
Roles.MATHML_STACK_CARRY,
|
||||
Roles.MATHML_STACK_LINE
|
||||
]),
|
||||
|
||||
objectOutputFunctions: {
|
||||
_generateBaseOutput:
|
||||
function _generateBaseOutput(aAccessible, aRoleStr, aState, aFlags) {
|
||||
let output = [];
|
||||
|
||||
if (aFlags & INCLUDE_DESC) {
|
||||
this._addState(output, aState, aRoleStr);
|
||||
this._addType(output, aAccessible, aRoleStr);
|
||||
this._addRole(output, aAccessible, aRoleStr);
|
||||
}
|
||||
|
||||
if (aFlags & INCLUDE_VALUE && aAccessible.value.trim()) {
|
||||
output[this.outputOrder === OUTPUT_DESC_FIRST ? "push" : "unshift"](
|
||||
aAccessible.value);
|
||||
}
|
||||
|
||||
this._addName(output, aAccessible, aFlags);
|
||||
this._addLandmark(output, aAccessible);
|
||||
|
||||
return output;
|
||||
},
|
||||
|
||||
label: function label(aAccessible, aRoleStr, aState, aFlags, aContext) {
|
||||
if (aContext.isNestedControl ||
|
||||
aContext.accessible == Utils.getEmbeddedControl(aAccessible)) {
|
||||
// If we are on a nested control, or a nesting label,
|
||||
// we don't need the context.
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.objectOutputFunctions.defaultFunc.apply(this, arguments);
|
||||
},
|
||||
|
||||
entry: function entry(aAccessible, aRoleStr, aState, aFlags) {
|
||||
let rolestr = aState.contains(States.MULTI_LINE) ? "textarea" : "entry";
|
||||
return this.objectOutputFunctions.defaultFunc.apply(
|
||||
this, [aAccessible, rolestr, aState, aFlags]);
|
||||
},
|
||||
|
||||
pagetab: function pagetab(aAccessible, aRoleStr, aState, aFlags) {
|
||||
let itemno = {};
|
||||
let itemof = {};
|
||||
aAccessible.groupPosition({}, itemof, itemno);
|
||||
let output = [];
|
||||
this._addState(output, aState);
|
||||
this._addRole(output, aAccessible, aRoleStr);
|
||||
output.push({
|
||||
string: "objItemOfN",
|
||||
args: [itemno.value, itemof.value]
|
||||
});
|
||||
|
||||
this._addName(output, aAccessible, aFlags);
|
||||
this._addLandmark(output, aAccessible);
|
||||
|
||||
return output;
|
||||
},
|
||||
|
||||
table: function table(aAccessible, aRoleStr, aState, aFlags) {
|
||||
let output = [];
|
||||
let table;
|
||||
try {
|
||||
table = aAccessible.QueryInterface(Ci.nsIAccessibleTable);
|
||||
} catch (x) {
|
||||
Logger.logException(x);
|
||||
return output;
|
||||
} finally {
|
||||
// Check if it's a layout table, and bail out if true.
|
||||
// We don't want to speak any table information for layout tables.
|
||||
if (table.isProbablyForLayout()) {
|
||||
return output;
|
||||
}
|
||||
this._addRole(output, aAccessible, aRoleStr);
|
||||
output.push.call(output, {
|
||||
string: this._getOutputName("tblColumnInfo"),
|
||||
count: table.columnCount
|
||||
}, {
|
||||
string: this._getOutputName("tblRowInfo"),
|
||||
count: table.rowCount
|
||||
});
|
||||
this._addName(output, aAccessible, aFlags);
|
||||
this._addLandmark(output, aAccessible);
|
||||
return output;
|
||||
}
|
||||
},
|
||||
|
||||
gridcell: function gridcell(aAccessible, aRoleStr, aState, aFlags) {
|
||||
let output = [];
|
||||
this._addState(output, aState);
|
||||
this._addName(output, aAccessible, aFlags);
|
||||
this._addLandmark(output, aAccessible);
|
||||
return output;
|
||||
},
|
||||
|
||||
// Use the table output functions for MathML tabular elements.
|
||||
mathmltable: function mathmltable() {
|
||||
return this.objectOutputFunctions.table.apply(this, arguments);
|
||||
},
|
||||
|
||||
mathmlcell: function mathmlcell() {
|
||||
return this.objectOutputFunctions.cell.apply(this, arguments);
|
||||
},
|
||||
|
||||
mathmlenclosed: function mathmlenclosed(aAccessible, aRoleStr, aState,
|
||||
aFlags, aContext) {
|
||||
let output = this.objectOutputFunctions.defaultFunc.
|
||||
apply(this, [aAccessible, aRoleStr, aState, aFlags, aContext]);
|
||||
this._addMencloseNotations(output, aAccessible);
|
||||
return output;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates speech utterances from objects, actions and state changes.
|
||||
* An utterance is an array of speech data.
|
||||
*
|
||||
* It should not be assumed that flattening an utterance array would create a
|
||||
* gramatically correct sentence. For example, {@link genForObject} might
|
||||
* return: ['graphic', 'Welcome to my home page'].
|
||||
* Each string element in an utterance should be gramatically correct in itself.
|
||||
* Another example from {@link genForObject}: ['list item 2 of 5', 'Alabama'].
|
||||
*
|
||||
* An utterance is ordered from the least to the most important. Speaking the
|
||||
* last string usually makes sense, but speaking the first often won't.
|
||||
* For example {@link genForAction} might return ['button', 'clicked'] for a
|
||||
* clicked event. Speaking only 'clicked' makes sense. Speaking 'button' does
|
||||
* not.
|
||||
*/
|
||||
var UtteranceGenerator = { // jshint ignore:line
|
||||
__proto__: OutputGenerator, // jshint ignore:line
|
||||
|
||||
gActionMap: {
|
||||
jump: "jumpAction",
|
||||
press: "pressAction",
|
||||
check: "checkAction",
|
||||
uncheck: "uncheckAction",
|
||||
on: "onAction",
|
||||
off: "offAction",
|
||||
select: "selectAction",
|
||||
unselect: "unselectAction",
|
||||
open: "openAction",
|
||||
close: "closeAction",
|
||||
switch: "switchAction",
|
||||
click: "clickAction",
|
||||
collapse: "collapseAction",
|
||||
expand: "expandAction",
|
||||
activate: "activateAction",
|
||||
cycle: "cycleAction"
|
||||
},
|
||||
|
||||
// TODO: May become more verbose in the future.
|
||||
genForAction: function genForAction(aObject, aActionName) {
|
||||
return [{string: this.gActionMap[aActionName]}];
|
||||
},
|
||||
|
||||
genForLiveRegion:
|
||||
function genForLiveRegion(aContext, aIsHide, aModifiedText) {
|
||||
let utterance = [];
|
||||
if (aIsHide) {
|
||||
utterance.push({string: "hidden"});
|
||||
}
|
||||
return utterance.concat(aModifiedText || this.genForContext(aContext));
|
||||
},
|
||||
|
||||
genForAnnouncement: function genForAnnouncement(aAnnouncement) {
|
||||
return [{
|
||||
string: aAnnouncement
|
||||
}];
|
||||
},
|
||||
|
||||
genForTabStateChange: function genForTabStateChange(aObject, aTabState) {
|
||||
switch (aTabState) {
|
||||
case "newtab":
|
||||
return [{string: "tabNew"}];
|
||||
case "loading":
|
||||
return [{string: "tabLoading"}];
|
||||
case "loaded":
|
||||
return [aObject.name, {string: "tabLoaded"}];
|
||||
case "loadstopped":
|
||||
return [{string: "tabLoadStopped"}];
|
||||
case "reload":
|
||||
return [{string: "tabReload"}];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
genForEditingMode: function genForEditingMode(aIsEditing) {
|
||||
return [{string: aIsEditing ? "editingMode" : "navigationMode"}];
|
||||
},
|
||||
|
||||
objectOutputFunctions: {
|
||||
|
||||
__proto__: OutputGenerator.objectOutputFunctions, // jshint ignore:line
|
||||
|
||||
defaultFunc: function defaultFunc() {
|
||||
return this.objectOutputFunctions._generateBaseOutput.apply(
|
||||
this, arguments);
|
||||
},
|
||||
|
||||
heading: function heading(aAccessible, aRoleStr, aState, aFlags) {
|
||||
let level = {};
|
||||
aAccessible.groupPosition(level, {}, {});
|
||||
let utterance = [{string: "headingLevel", args: [level.value]}];
|
||||
|
||||
this._addName(utterance, aAccessible, aFlags);
|
||||
this._addLandmark(utterance, aAccessible);
|
||||
|
||||
return utterance;
|
||||
},
|
||||
|
||||
listitem: function listitem(aAccessible, aRoleStr, aState, aFlags) {
|
||||
let itemno = {};
|
||||
let itemof = {};
|
||||
aAccessible.groupPosition({}, itemof, itemno);
|
||||
let utterance = [];
|
||||
if (itemno.value == 1) {
|
||||
// Start of list
|
||||
utterance.push({string: "listStart"});
|
||||
} else if (itemno.value == itemof.value) {
|
||||
// last item
|
||||
utterance.push({string: "listEnd"});
|
||||
}
|
||||
|
||||
this._addName(utterance, aAccessible, aFlags);
|
||||
this._addLandmark(utterance, aAccessible);
|
||||
|
||||
return utterance;
|
||||
},
|
||||
|
||||
list: function list(aAccessible, aRoleStr, aState, aFlags) {
|
||||
return this._getListUtterance(aAccessible, aRoleStr, aFlags,
|
||||
aAccessible.childCount);
|
||||
},
|
||||
|
||||
definitionlist:
|
||||
function definitionlist(aAccessible, aRoleStr, aState, aFlags) {
|
||||
return this._getListUtterance(aAccessible, aRoleStr, aFlags,
|
||||
aAccessible.childCount / 2);
|
||||
},
|
||||
|
||||
application: function application(aAccessible, aRoleStr, aState, aFlags) {
|
||||
// Don't utter location of applications, it gets tiring.
|
||||
if (aAccessible.name != aAccessible.DOMNode.location) {
|
||||
return this.objectOutputFunctions.defaultFunc.apply(this,
|
||||
[aAccessible, aRoleStr, aState, aFlags]);
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
|
||||
cell: function cell(aAccessible, aRoleStr, aState, aFlags, aContext) {
|
||||
let utterance = [];
|
||||
let cell = aContext.getCellInfo(aAccessible);
|
||||
if (cell) {
|
||||
let addCellChanged =
|
||||
function addCellChanged(aUtterance, aChanged, aString, aIndex) {
|
||||
if (aChanged) {
|
||||
aUtterance.push({string: aString, args: [aIndex + 1]});
|
||||
}
|
||||
};
|
||||
let addExtent = function addExtent(aUtterance, aExtent, aString) {
|
||||
if (aExtent > 1) {
|
||||
aUtterance.push({string: aString, args: [aExtent]});
|
||||
}
|
||||
};
|
||||
let addHeaders = function addHeaders(aUtterance, aHeaders) {
|
||||
if (aHeaders.length > 0) {
|
||||
aUtterance.push.apply(aUtterance, aHeaders);
|
||||
}
|
||||
};
|
||||
|
||||
addCellChanged(utterance, cell.columnChanged, "columnInfo",
|
||||
cell.columnIndex);
|
||||
addCellChanged(utterance, cell.rowChanged, "rowInfo", cell.rowIndex);
|
||||
|
||||
addExtent(utterance, cell.columnExtent, "spansColumns");
|
||||
addExtent(utterance, cell.rowExtent, "spansRows");
|
||||
|
||||
addHeaders(utterance, cell.columnHeaders);
|
||||
addHeaders(utterance, cell.rowHeaders);
|
||||
}
|
||||
|
||||
this._addName(utterance, aAccessible, aFlags);
|
||||
this._addLandmark(utterance, aAccessible);
|
||||
|
||||
return utterance;
|
||||
},
|
||||
|
||||
columnheader: function columnheader() {
|
||||
return this.objectOutputFunctions.cell.apply(this, arguments);
|
||||
},
|
||||
|
||||
rowheader: function rowheader() {
|
||||
return this.objectOutputFunctions.cell.apply(this, arguments);
|
||||
},
|
||||
|
||||
statictext: function statictext(aAccessible) {
|
||||
if (Utils.isListItemDecorator(aAccessible, true)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.objectOutputFunctions.defaultFunc.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
|
||||
_getContextStart: function _getContextStart(aContext) {
|
||||
return aContext.newAncestry;
|
||||
},
|
||||
|
||||
_addRole: function _addRole(aOutput, aAccessible, aRoleStr) {
|
||||
if (this.mathmlRolesSet.has(aAccessible.role)) {
|
||||
this._addMathRoles(aOutput, aAccessible, aRoleStr);
|
||||
} else {
|
||||
aOutput.push({string: this._getOutputName(aRoleStr)});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add localized state information to output data.
|
||||
* Note: We do not expose checked and selected states, we let TalkBack do it for us
|
||||
* there. This is because we expose the checked information on the node info itself.
|
||||
*/
|
||||
_addState: function _addState(aOutput, aState, aRoleStr) {
|
||||
if (aState.contains(States.UNAVAILABLE)) {
|
||||
aOutput.push({string: "stateUnavailable"});
|
||||
}
|
||||
|
||||
if (aState.contains(States.READONLY)) {
|
||||
aOutput.push({string: "stateReadonly"});
|
||||
}
|
||||
|
||||
if (aState.contains(States.PRESSED)) {
|
||||
aOutput.push({string: "statePressed"});
|
||||
}
|
||||
|
||||
if (aState.contains(States.EXPANDABLE)) {
|
||||
let statetr = aState.contains(States.EXPANDED) ?
|
||||
"stateExpanded" : "stateCollapsed";
|
||||
aOutput.push({string: statetr});
|
||||
}
|
||||
|
||||
if (aState.contains(States.REQUIRED)) {
|
||||
aOutput.push({string: "stateRequired"});
|
||||
}
|
||||
|
||||
if (aState.contains(States.TRAVERSED)) {
|
||||
aOutput.push({string: "stateTraversed"});
|
||||
}
|
||||
|
||||
if (aState.contains(States.HASPOPUP)) {
|
||||
aOutput.push({string: "stateHasPopup"});
|
||||
}
|
||||
},
|
||||
|
||||
_getListUtterance:
|
||||
function _getListUtterance(aAccessible, aRoleStr, aFlags, aItemCount) {
|
||||
let utterance = [];
|
||||
this._addRole(utterance, aAccessible, aRoleStr);
|
||||
utterance.push({
|
||||
string: this._getOutputName("listItemsCount"),
|
||||
count: aItemCount
|
||||
});
|
||||
|
||||
this._addName(utterance, aAccessible, aFlags);
|
||||
this._addLandmark(utterance, aAccessible);
|
||||
|
||||
return utterance;
|
||||
}
|
||||
};
|
@ -1,332 +0,0 @@
|
||||
/* 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();
|
@ -9,8 +9,6 @@ EXTRA_JS_MODULES.accessibility += [
|
||||
'Constants.jsm',
|
||||
'ContentControl.jsm',
|
||||
'EventManager.jsm',
|
||||
'OutputGenerator.jsm',
|
||||
'Presentation.jsm',
|
||||
'Traversal.jsm',
|
||||
'Utils.jsm'
|
||||
]
|
||||
|
@ -10,19 +10,28 @@ support-files =
|
||||
|
||||
[test_alive.html]
|
||||
[test_content_integration.html]
|
||||
skip-if = true
|
||||
[test_explicit_names.html]
|
||||
skip-if = true
|
||||
[test_hints.html]
|
||||
skip-if = true
|
||||
[test_landmarks.html]
|
||||
skip-if = true
|
||||
[test_live_regions.html]
|
||||
skip-if = true
|
||||
[test_output_mathml.html]
|
||||
skip-if = true
|
||||
[test_output.html]
|
||||
skip-if = true
|
||||
[test_tables.html]
|
||||
skip-if = true
|
||||
[test_text_editable_navigation.html]
|
||||
skip-if = (verify && !debug && (os == 'linux'))
|
||||
skip-if = true
|
||||
[test_text_editing.html]
|
||||
skip-if = (verify && !debug && (os == 'linux'))
|
||||
skip-if = true
|
||||
[test_text_navigation_focus.html]
|
||||
skip-if = (verify && !debug && (os == 'linux'))
|
||||
skip-if = true
|
||||
[test_text_navigation.html]
|
||||
skip-if = true
|
||||
[test_traversal.html]
|
||||
[test_traversal_helper.html]
|
||||
|
@ -34,6 +34,7 @@ import org.hamcrest.Matchers.*
|
||||
import org.junit.Test
|
||||
import org.junit.Before
|
||||
import org.junit.After
|
||||
import org.junit.Ignore
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
const val DISPLAY_WIDTH = 480
|
||||
@ -154,7 +155,7 @@ class AccessibilityTest : BaseSessionTest() {
|
||||
node.className.toString(), equalTo("android.webkit.WebView"))
|
||||
}
|
||||
|
||||
@Test fun testPageLoad() {
|
||||
@Ignore @Test fun testPageLoad() {
|
||||
sessionRule.session.loadTestPath(INPUTS_PATH)
|
||||
|
||||
sessionRule.waitUntilCalled(object : EventDelegate {
|
||||
@ -163,7 +164,7 @@ class AccessibilityTest : BaseSessionTest() {
|
||||
})
|
||||
}
|
||||
|
||||
@Test fun testAccessibilityFocus() {
|
||||
@Ignore @Test fun testAccessibilityFocus() {
|
||||
var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID
|
||||
sessionRule.session.loadTestPath(INPUTS_PATH)
|
||||
waitForInitialFocus()
|
||||
@ -193,7 +194,7 @@ class AccessibilityTest : BaseSessionTest() {
|
||||
})
|
||||
}
|
||||
|
||||
@Test fun testTextEntryNode() {
|
||||
@Ignore @Test fun testTextEntryNode() {
|
||||
sessionRule.session.loadString("<input aria-label='Name' value='Tobias'>", "text/html")
|
||||
waitForInitialFocus()
|
||||
|
||||
@ -275,7 +276,7 @@ class AccessibilityTest : BaseSessionTest() {
|
||||
return arguments
|
||||
}
|
||||
|
||||
@Test fun testClipboard() {
|
||||
@Ignore @Test fun testClipboard() {
|
||||
var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID;
|
||||
sessionRule.session.loadString("<input value='hello cruel world' id='input'>", "text/html")
|
||||
waitForInitialFocus()
|
||||
@ -326,7 +327,7 @@ class AccessibilityTest : BaseSessionTest() {
|
||||
})
|
||||
}
|
||||
|
||||
@Test fun testMoveByCharacter() {
|
||||
@Ignore @Test fun testMoveByCharacter() {
|
||||
var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID
|
||||
sessionRule.session.loadTestPath(LOREM_IPSUM_HTML_PATH)
|
||||
waitForInitialFocus()
|
||||
@ -359,7 +360,7 @@ class AccessibilityTest : BaseSessionTest() {
|
||||
waitUntilTextTraversed(0, 1) // "L"
|
||||
}
|
||||
|
||||
@Test fun testMoveByWord() {
|
||||
@Ignore @Test fun testMoveByWord() {
|
||||
var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID
|
||||
sessionRule.session.loadTestPath(LOREM_IPSUM_HTML_PATH)
|
||||
waitForInitialFocus()
|
||||
@ -392,7 +393,7 @@ class AccessibilityTest : BaseSessionTest() {
|
||||
waitUntilTextTraversed(0, 5) // "Lorem"
|
||||
}
|
||||
|
||||
@Test fun testMoveByLine() {
|
||||
@Ignore @Test fun testMoveByLine() {
|
||||
var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID
|
||||
sessionRule.session.loadTestPath(LOREM_IPSUM_HTML_PATH)
|
||||
waitForInitialFocus()
|
||||
@ -425,7 +426,7 @@ class AccessibilityTest : BaseSessionTest() {
|
||||
waitUntilTextTraversed(0, 18) // "Lorem ipsum dolor "
|
||||
}
|
||||
|
||||
@Test fun testCheckbox() {
|
||||
@Ignore @Test fun testCheckbox() {
|
||||
var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID;
|
||||
sessionRule.session.loadString("<label><input id='checkbox' type='checkbox'>many option</label>", "text/html")
|
||||
waitForInitialFocus()
|
||||
@ -451,7 +452,7 @@ class AccessibilityTest : BaseSessionTest() {
|
||||
waitUntilClick(false)
|
||||
}
|
||||
|
||||
@Test fun testSelectable() {
|
||||
@Ignore @Test fun testSelectable() {
|
||||
var nodeId = View.NO_ID
|
||||
sessionRule.session.loadString(
|
||||
"""<ul style="list-style-type: none;" role="listbox">
|
||||
@ -492,7 +493,7 @@ class AccessibilityTest : BaseSessionTest() {
|
||||
return screenRect.contains(nodeBounds)
|
||||
}
|
||||
|
||||
@Test fun testScroll() {
|
||||
@Ignore @Test fun testScroll() {
|
||||
var nodeId = View.NO_ID
|
||||
sessionRule.session.loadString(
|
||||
"""<body style="margin: 0;">
|
||||
@ -587,7 +588,7 @@ class AccessibilityTest : BaseSessionTest() {
|
||||
|
||||
@ReuseSession(false) // XXX automation crash fix (bug 1485107)
|
||||
@WithDevToolsAPI
|
||||
@Test fun autoFill() {
|
||||
@Ignore @Test fun autoFill() {
|
||||
// Wait for the accessibility nodes to populate.
|
||||
mainSession.loadTestPath(FORMS_HTML_PATH)
|
||||
sessionRule.waitUntilCalled(object : EventDelegate {
|
||||
@ -667,7 +668,7 @@ class AccessibilityTest : BaseSessionTest() {
|
||||
}
|
||||
|
||||
@ReuseSession(false) // XXX automation crash fix (bug 1485107)
|
||||
@Test fun autoFill_navigation() {
|
||||
@Ignore @Test fun autoFill_navigation() {
|
||||
fun countAutoFillNodes(cond: (AccessibilityNodeInfo) -> Boolean =
|
||||
{ it.className == "android.widget.EditText" },
|
||||
id: Int = View.NO_ID): Int {
|
||||
|
@ -1084,9 +1084,6 @@ public class GeckoSession extends LayerSession
|
||||
mTextInput.onWindowChanged(mWindow);
|
||||
}
|
||||
if ((change == WINDOW_CLOSE || change == WINDOW_TRANSFER_OUT) && !inProgress) {
|
||||
if (mAccessibility != null) {
|
||||
mAccessibility.clearAutoFill();
|
||||
}
|
||||
mTextInput.clearAutoFill();
|
||||
}
|
||||
}
|
||||
@ -1416,10 +1413,6 @@ public class GeckoSession extends LayerSession
|
||||
final GeckoBundle msg = new GeckoBundle(1);
|
||||
msg.putBoolean("focused", focused);
|
||||
mEventDispatcher.dispatch("GeckoView:SetFocused", msg);
|
||||
|
||||
if (focused && mAccessibility != null) {
|
||||
mAccessibility.onWindowFocus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -19,11 +19,8 @@ import android.graphics.Matrix;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.InputType;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.InputDevice;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
@ -37,10 +34,6 @@ public class SessionAccessibility {
|
||||
private static final String LOGTAG = "GeckoAccessibility";
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
// This is a special ID we use for nodes that are event sources.
|
||||
// We expose it as a fragment and not an actual child of the View node.
|
||||
private static final int VIRTUAL_CONTENT_ID = -2;
|
||||
|
||||
// This is the number BrailleBack uses to start indexing routing keys.
|
||||
private static final int BRAILLE_CLICK_BASE_INDEX = -275000000;
|
||||
|
||||
@ -51,105 +44,23 @@ public class SessionAccessibility {
|
||||
/* package */ final class NodeProvider extends AccessibilityNodeProvider {
|
||||
@Override
|
||||
public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualDescendantId) {
|
||||
AccessibilityNodeInfo info = getAutoFillNode(virtualDescendantId);
|
||||
if (info != null) {
|
||||
// Try auto-fill nodes first.
|
||||
return info;
|
||||
AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(mView, virtualDescendantId);
|
||||
if (Build.VERSION.SDK_INT < 17 || mView.getDisplay() != null) {
|
||||
// When running junit tests we don't have a display
|
||||
mView.onInitializeAccessibilityNodeInfo(node);
|
||||
}
|
||||
|
||||
info = (virtualDescendantId == VIRTUAL_CONTENT_ID && mVirtualContentNode != null)
|
||||
? AccessibilityNodeInfo.obtain(mVirtualContentNode)
|
||||
: AccessibilityNodeInfo.obtain(mView, virtualDescendantId);
|
||||
|
||||
switch (virtualDescendantId) {
|
||||
case View.NO_ID:
|
||||
// This is the parent View node.
|
||||
// We intentionally don't add VIRTUAL_CONTENT_ID
|
||||
// as a child. It is a source for events,
|
||||
// but not a member of the tree you
|
||||
// can get to by traversing down.
|
||||
if (Build.VERSION.SDK_INT < 17 || mView.getDisplay() != null) {
|
||||
// When running junit tests we don't have a display
|
||||
mView.onInitializeAccessibilityNodeInfo(info);
|
||||
}
|
||||
info.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
|
||||
info.setClassName("android.webkit.WebView"); // TODO: WTF
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 19) {
|
||||
Bundle bundle = info.getExtras();
|
||||
bundle.putCharSequence(
|
||||
"ACTION_ARGUMENT_HTML_ELEMENT_STRING_VALUES",
|
||||
"ARTICLE,BUTTON,CHECKBOX,COMBOBOX,CONTROL," +
|
||||
"FOCUSABLE,FRAME,GRAPHIC,H1,H2,H3,H4,H5,H6," +
|
||||
"HEADING,LANDMARK,LINK,LIST,LIST_ITEM,MAIN," +
|
||||
"MEDIA,RADIO,SECTION,TABLE,TEXT_FIELD," +
|
||||
"UNVISITED_LINK,VISITED_LINK");
|
||||
}
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
|
||||
|
||||
if (mAutoFillRoots != null) {
|
||||
// Add auto-fill nodes.
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "Adding roots " + mAutoFillRoots);
|
||||
}
|
||||
for (int i = 0; i < mAutoFillRoots.size(); i++) {
|
||||
info.addChild(mView, mAutoFillRoots.keyAt(i));
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
info.setParent(mView);
|
||||
info.setSource(mView, virtualDescendantId);
|
||||
info.setVisibleToUser(mView.isShown());
|
||||
info.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
|
||||
info.setEnabled(true);
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
|
||||
info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER |
|
||||
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD |
|
||||
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE);
|
||||
break;
|
||||
}
|
||||
return info;
|
||||
node.setClassName("android.webkit.WebView");
|
||||
return node;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performAction(int virtualViewId, int action, Bundle arguments) {
|
||||
if (virtualViewId == View.NO_ID) {
|
||||
return performRootAction(action, arguments);
|
||||
}
|
||||
if (action == AccessibilityNodeInfo.ACTION_SET_TEXT) {
|
||||
final String value = arguments.getString(Build.VERSION.SDK_INT >= 21
|
||||
? AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE
|
||||
: ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE);
|
||||
return performAutoFill(virtualViewId, value);
|
||||
}
|
||||
return performContentAction(action, arguments);
|
||||
}
|
||||
|
||||
private boolean performRootAction(int action, Bundle arguments) {
|
||||
switch (action) {
|
||||
case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
|
||||
case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
|
||||
final GeckoBundle data = new GeckoBundle(1);
|
||||
data.putBoolean("gainFocus", action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
|
||||
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityViewFocused", data);
|
||||
return true;
|
||||
}
|
||||
|
||||
return mView.performAccessibilityAction(action, arguments);
|
||||
}
|
||||
|
||||
@SuppressWarnings("fallthrough")
|
||||
private boolean performContentAction(int action, Bundle arguments) {
|
||||
public boolean performAction(final int virtualViewId, int action, Bundle arguments) {
|
||||
final GeckoBundle data;
|
||||
switch (action) {
|
||||
case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
|
||||
final AccessibilityEvent event = obtainEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED, VIRTUAL_CONTENT_ID);
|
||||
final AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
|
||||
event.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
|
||||
event.setSource(mView, virtualViewId);
|
||||
((ViewParent) mView).requestSendAccessibilityEvent(mView, event);
|
||||
return true;
|
||||
case AccessibilityNodeInfo.ACTION_CLICK:
|
||||
@ -168,10 +79,6 @@ public class SessionAccessibility {
|
||||
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilitySelect", null);
|
||||
return true;
|
||||
case AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT:
|
||||
if (mLastItem) {
|
||||
return false;
|
||||
}
|
||||
// fall-through
|
||||
case AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT:
|
||||
if (arguments != null) {
|
||||
data = new GeckoBundle(1);
|
||||
@ -236,15 +143,6 @@ public class SessionAccessibility {
|
||||
private View mView;
|
||||
// The native portion of the node provider.
|
||||
/* package */ final NativeProvider nativeProvider = new NativeProvider();
|
||||
// Have we reached the last item in content?
|
||||
private boolean mLastItem;
|
||||
// Used to store the JSON message and populate the event later in the code path.
|
||||
private AccessibilityNodeInfo mVirtualContentNode;
|
||||
// Auto-fill nodes.
|
||||
private SparseArray<GeckoBundle> mAutoFillNodes;
|
||||
private SparseArray<EventCallback> mAutoFillRoots;
|
||||
private int mAutoFillFocusedId = View.NO_ID;
|
||||
private int mAutoFillFocusedRoot = View.NO_ID;
|
||||
|
||||
private boolean mAttached = false;
|
||||
|
||||
@ -252,27 +150,6 @@ public class SessionAccessibility {
|
||||
mSession = session;
|
||||
|
||||
Settings.updateAccessibilitySettings();
|
||||
|
||||
session.getEventDispatcher().registerUiThreadListener(new BundleEventListener() {
|
||||
@Override
|
||||
public void handleMessage(final String event, final GeckoBundle message,
|
||||
final EventCallback callback) {
|
||||
if ("GeckoView:AccessibilityEvent".equals(event)) {
|
||||
sendAccessibilityEvent(message);
|
||||
} else if ("GeckoView:AddAutoFill".equals(event)) {
|
||||
addAutoFill(message, callback);
|
||||
} else if ("GeckoView:ClearAutoFill".equals(event)) {
|
||||
clearAutoFill();
|
||||
} else if ("GeckoView:OnAutoFillFocus".equals(event)) {
|
||||
onAutoFillFocus(message);
|
||||
}
|
||||
}
|
||||
},
|
||||
"GeckoView:AccessibilityEvent",
|
||||
"GeckoView:AddAutoFill",
|
||||
"GeckoView:ClearAutoFill",
|
||||
"GeckoView:OnAutoFillFocus",
|
||||
null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -295,7 +172,6 @@ public class SessionAccessibility {
|
||||
}
|
||||
|
||||
mView = view;
|
||||
mLastItem = false;
|
||||
|
||||
if (mView == null) {
|
||||
return;
|
||||
@ -387,164 +263,6 @@ public class SessionAccessibility {
|
||||
}
|
||||
}
|
||||
|
||||
private AccessibilityEvent obtainEvent(final int eventType, final int sourceId) {
|
||||
AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
|
||||
event.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
|
||||
event.setSource(mView, sourceId);
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
private static void populateEventFromJSON(AccessibilityEvent event, final GeckoBundle message) {
|
||||
final String[] textArray = message.getStringArray("text");
|
||||
if (textArray != null) {
|
||||
for (int i = 0; i < textArray.length; i++)
|
||||
event.getText().add(textArray[i] != null ? textArray[i] : "");
|
||||
}
|
||||
|
||||
if (message.containsKey("className"))
|
||||
event.setClassName(message.getString("className"));
|
||||
event.setContentDescription(message.getString("description", ""));
|
||||
event.setEnabled(message.getBoolean("enabled", true));
|
||||
event.setChecked(message.getBoolean("checked"));
|
||||
event.setPassword(message.getBoolean("password"));
|
||||
event.setAddedCount(message.getInt("addedCount", -1));
|
||||
event.setRemovedCount(message.getInt("removedCount", -1));
|
||||
event.setFromIndex(message.getInt("fromIndex", -1));
|
||||
event.setItemCount(message.getInt("itemCount", -1));
|
||||
event.setCurrentItemIndex(message.getInt("currentItemIndex", -1));
|
||||
event.setBeforeText(message.getString("beforeText", ""));
|
||||
event.setToIndex(message.getInt("toIndex", -1));
|
||||
event.setScrollable(message.getBoolean("scrollable"));
|
||||
event.setScrollX(message.getInt("scrollX", -1));
|
||||
event.setScrollY(message.getInt("scrollY", -1));
|
||||
event.setMaxScrollX(message.getInt("maxScrollX", -1));
|
||||
event.setMaxScrollY(message.getInt("maxScrollY", -1));
|
||||
}
|
||||
|
||||
private void populateNodeInfoFromJSON(AccessibilityNodeInfo node, final GeckoBundle message) {
|
||||
node.setEnabled(message.getBoolean("enabled", true));
|
||||
node.setCheckable(message.getBoolean("checkable"));
|
||||
node.setChecked(message.getBoolean("checked"));
|
||||
node.setPassword(message.getBoolean("password"));
|
||||
node.setFocusable(message.getBoolean("focusable"));
|
||||
node.setFocused(message.getBoolean("focused"));
|
||||
node.setSelected(message.getBoolean("selected"));
|
||||
|
||||
node.setClassName(message.getString("className", "android.view.View"));
|
||||
|
||||
final String[] textArray = message.getStringArray("text");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (textArray != null && textArray.length > 0) {
|
||||
sb.append(textArray[0] != null ? textArray[0] : "");
|
||||
for (int i = 1; i < textArray.length; i++) {
|
||||
sb.append(' ').append(textArray[i] != null ? textArray[i] : "");
|
||||
}
|
||||
node.setText(sb.toString());
|
||||
}
|
||||
node.setContentDescription(message.getString("description", ""));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 18 && message.getBoolean("editable")) {
|
||||
node.addAction(AccessibilityNodeInfo.ACTION_SET_SELECTION);
|
||||
node.addAction(AccessibilityNodeInfo.ACTION_CUT);
|
||||
node.addAction(AccessibilityNodeInfo.ACTION_COPY);
|
||||
node.addAction(AccessibilityNodeInfo.ACTION_PASTE);
|
||||
node.setEditable(true);
|
||||
}
|
||||
|
||||
if (message.getBoolean("clickable")) {
|
||||
node.setClickable(true);
|
||||
node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 19 && message.containsKey("hint")) {
|
||||
Bundle bundle = node.getExtras();
|
||||
bundle.putCharSequence("AccessibilityNodeInfo.hint", message.getString("hint"));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBounds(final AccessibilityNodeInfo node, final GeckoBundle message) {
|
||||
final GeckoBundle bounds = message.getBundle("bounds");
|
||||
if (bounds == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Rect screenBounds = new Rect(bounds.getInt("left"), bounds.getInt("top"),
|
||||
bounds.getInt("right"), bounds.getInt("bottom"));
|
||||
node.setBoundsInScreen(screenBounds);
|
||||
|
||||
final Matrix matrix = new Matrix();
|
||||
final float[] origin = new float[2];
|
||||
mSession.getClientToScreenMatrix(matrix);
|
||||
matrix.mapPoints(origin);
|
||||
|
||||
screenBounds.offset((int) -origin[0], (int) -origin[1]);
|
||||
node.setBoundsInParent(screenBounds);
|
||||
}
|
||||
|
||||
private void updateState(final AccessibilityNodeInfo node, final GeckoBundle message) {
|
||||
if (message.containsKey("checked")) {
|
||||
node.setChecked(message.getBoolean("checked"));
|
||||
}
|
||||
if (message.containsKey("selected")) {
|
||||
node.setSelected(message.getBoolean("selected"));
|
||||
}
|
||||
}
|
||||
|
||||
private void sendAccessibilityEvent(final GeckoBundle message) {
|
||||
if (mView == null || !Settings.isTouchExplorationEnabled())
|
||||
return;
|
||||
|
||||
final int eventType = message.getInt("eventType", -1);
|
||||
if (eventType < 0) {
|
||||
Log.e(LOGTAG, "No accessibility event type provided");
|
||||
return;
|
||||
}
|
||||
|
||||
int eventSource = VIRTUAL_CONTENT_ID;
|
||||
|
||||
if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
|
||||
final String exitView = message.getString("exitView", "");
|
||||
|
||||
mLastItem = exitView.equals("moveNext");
|
||||
if (mLastItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (exitView.equals("movePrevious")) {
|
||||
eventSource = View.NO_ID;
|
||||
}
|
||||
}
|
||||
|
||||
if (eventSource != View.NO_ID &&
|
||||
(eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED ||
|
||||
eventType == AccessibilityEvent.TYPE_VIEW_FOCUSED ||
|
||||
eventType == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)) {
|
||||
// In Jelly Bean we populate an AccessibilityNodeInfo with the minimal amount of data to have
|
||||
// it work with TalkBack.
|
||||
if (mVirtualContentNode != null) {
|
||||
mVirtualContentNode.recycle();
|
||||
}
|
||||
mVirtualContentNode = AccessibilityNodeInfo.obtain(mView, eventSource);
|
||||
populateNodeInfoFromJSON(mVirtualContentNode, message);
|
||||
}
|
||||
|
||||
if (mVirtualContentNode != null) {
|
||||
// Bounds for the virtual content can be updated from any event.
|
||||
updateBounds(mVirtualContentNode, message);
|
||||
|
||||
// State for the virtual content can be updated when view is clicked/selected.
|
||||
if (eventType == AccessibilityEvent.TYPE_VIEW_CLICKED ||
|
||||
eventType == AccessibilityEvent.TYPE_VIEW_SELECTED) {
|
||||
updateState(mVirtualContentNode, message);
|
||||
}
|
||||
}
|
||||
|
||||
final AccessibilityEvent accessibilityEvent = obtainEvent(eventType, eventSource);
|
||||
populateEventFromJSON(accessibilityEvent, message);
|
||||
((ViewParent) mView).requestSendAccessibilityEvent(mView, accessibilityEvent);
|
||||
}
|
||||
|
||||
public boolean onMotionEvent(final MotionEvent event) {
|
||||
if (!Settings.isTouchExplorationEnabled()) {
|
||||
return false;
|
||||
@ -567,245 +285,6 @@ public class SessionAccessibility {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* package */ AccessibilityNodeInfo getAutoFillNode(final int id) {
|
||||
if (mView == null || mAutoFillRoots == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final GeckoBundle bundle = mAutoFillNodes.get(id);
|
||||
if (bundle == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "getAutoFillNode(" + id + ')');
|
||||
}
|
||||
|
||||
final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(mView, id);
|
||||
node.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
|
||||
node.setParent(mView, bundle.getInt("parent", View.NO_ID));
|
||||
node.setEnabled(true);
|
||||
|
||||
if (mAutoFillFocusedRoot != View.NO_ID &&
|
||||
mAutoFillFocusedRoot == bundle.getInt("root", View.NO_ID)) {
|
||||
// Some auto-fill clients require a dummy rect for the focused View.
|
||||
final Rect rect = new Rect();
|
||||
mSession.getSurfaceBounds(rect);
|
||||
node.setVisibleToUser(!rect.isEmpty());
|
||||
node.setBoundsInParent(rect);
|
||||
|
||||
final int[] offset = new int[2];
|
||||
mView.getLocationOnScreen(offset);
|
||||
rect.offset(offset[0], offset[1]);
|
||||
node.setBoundsInScreen(rect);
|
||||
}
|
||||
|
||||
final GeckoBundle[] children = bundle.getBundleArray("children");
|
||||
if (children != null) {
|
||||
for (final GeckoBundle child : children) {
|
||||
final int childId = child.getInt("id");
|
||||
node.addChild(mView, childId);
|
||||
mAutoFillNodes.append(childId, child);
|
||||
}
|
||||
}
|
||||
|
||||
String tag = bundle.getString("tag", "");
|
||||
final String type = bundle.getString("type", "text");
|
||||
final GeckoBundle attrs = bundle.getBundle("attributes");
|
||||
|
||||
if ("INPUT".equals(tag) && !bundle.getBoolean("editable", false)) {
|
||||
tag = ""; // Don't process non-editable inputs (e.g. type="button").
|
||||
}
|
||||
switch (tag) {
|
||||
case "INPUT":
|
||||
case "TEXTAREA": {
|
||||
final boolean disabled = bundle.getBoolean("disabled");
|
||||
node.setClassName("android.widget.EditText");
|
||||
node.setEnabled(!disabled);
|
||||
node.setFocusable(!disabled);
|
||||
node.setFocused(id == mAutoFillFocusedId);
|
||||
|
||||
if ("password".equals(type)) {
|
||||
node.setPassword(true);
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 18) {
|
||||
node.setEditable(!disabled);
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 19) {
|
||||
node.setMultiLine("TEXTAREA".equals(tag));
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
try {
|
||||
node.setMaxTextLength(Integer.parseInt(
|
||||
String.valueOf(attrs.get("maxlength"))));
|
||||
} catch (final NumberFormatException ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!disabled) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
node.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT);
|
||||
} else {
|
||||
node.addAction(ACTION_SET_TEXT);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (children != null) {
|
||||
node.setClassName("android.view.ViewGroup");
|
||||
} else {
|
||||
node.setClassName("android.view.View");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 19 && "INPUT".equals(tag)) {
|
||||
switch (type) {
|
||||
case "email":
|
||||
node.setInputType(InputType.TYPE_CLASS_TEXT |
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS);
|
||||
break;
|
||||
case "number":
|
||||
node.setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||
break;
|
||||
case "password":
|
||||
node.setInputType(InputType.TYPE_CLASS_TEXT |
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD);
|
||||
break;
|
||||
case "tel":
|
||||
node.setInputType(InputType.TYPE_CLASS_PHONE);
|
||||
break;
|
||||
case "text":
|
||||
node.setInputType(InputType.TYPE_CLASS_TEXT |
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
|
||||
break;
|
||||
case "url":
|
||||
node.setInputType(InputType.TYPE_CLASS_TEXT |
|
||||
InputType.TYPE_TEXT_VARIATION_URI);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/* package */ boolean performAutoFill(final int id, final String value) {
|
||||
if (mAutoFillRoots == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int rootId = id;
|
||||
for (int currentId = id; currentId != View.NO_ID;) {
|
||||
final GeckoBundle bundle = mAutoFillNodes.get(currentId);
|
||||
if (bundle == null) {
|
||||
return false;
|
||||
}
|
||||
rootId = currentId;
|
||||
currentId = bundle.getInt("parent", View.NO_ID);
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "performAutoFill(" + id + ", " + value + ')');
|
||||
}
|
||||
|
||||
final EventCallback callback = mAutoFillRoots.get(rootId);
|
||||
if (callback == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final GeckoBundle response = new GeckoBundle(1);
|
||||
response.putString(String.valueOf(id), value);
|
||||
callback.sendSuccess(response);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void fireWindowChangedEvent(final int id) {
|
||||
if (Settings.isEnabled() && mView instanceof ViewParent) {
|
||||
final AccessibilityEvent event = obtainEvent(
|
||||
AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED, id);
|
||||
if (Build.VERSION.SDK_INT >= 19) {
|
||||
event.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
|
||||
}
|
||||
((ViewParent) mView).requestSendAccessibilityEvent(mView, event);
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ void addAutoFill(final GeckoBundle message, final EventCallback callback) {
|
||||
if (!Settings.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mAutoFillRoots == null) {
|
||||
mAutoFillRoots = new SparseArray<>();
|
||||
mAutoFillNodes = new SparseArray<>();
|
||||
}
|
||||
|
||||
final int id = message.getInt("id");
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "addAutoFill(" + id + ')');
|
||||
}
|
||||
|
||||
mAutoFillRoots.append(id, callback);
|
||||
mAutoFillNodes.append(id, message);
|
||||
fireWindowChangedEvent(id);
|
||||
}
|
||||
|
||||
/* package */ void clearAutoFill() {
|
||||
if (mAutoFillRoots != null) {
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "clearAutoFill()");
|
||||
}
|
||||
mAutoFillRoots = null;
|
||||
mAutoFillNodes = null;
|
||||
mAutoFillFocusedId = View.NO_ID;
|
||||
mAutoFillFocusedRoot = View.NO_ID;
|
||||
fireWindowChangedEvent(View.NO_ID);
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ void onAutoFillFocus(final GeckoBundle message) {
|
||||
if (!Settings.isEnabled() || !(mView instanceof ViewParent) || mAutoFillNodes == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int id;
|
||||
final int root;
|
||||
if (message != null) {
|
||||
id = message.getInt("id");
|
||||
root = message.getInt("root");
|
||||
} else {
|
||||
id = root = View.NO_ID;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "onAutoFillFocus(" + id + ')');
|
||||
}
|
||||
if (mAutoFillFocusedId == id) {
|
||||
return;
|
||||
}
|
||||
mAutoFillFocusedId = id;
|
||||
mAutoFillFocusedRoot = root;
|
||||
|
||||
// We already send "TYPE_VIEW_FOCUSED" in touch exploration mode,
|
||||
// so in that case don't send it here.
|
||||
if (!Settings.isTouchExplorationEnabled()) {
|
||||
AccessibilityEvent event = obtainEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED, id);
|
||||
((ViewParent) mView).requestSendAccessibilityEvent(mView, event);
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ void onWindowFocus() {
|
||||
// Auto-fill clients expect a state change event on focus.
|
||||
if (Settings.isEnabled() && mView instanceof ViewParent) {
|
||||
if (DEBUG) {
|
||||
Log.d(LOGTAG, "onWindowFocus()");
|
||||
}
|
||||
final AccessibilityEvent event = obtainEvent(
|
||||
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, View.NO_ID);
|
||||
((ViewParent) mView).requestSendAccessibilityEvent(mView, event);
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ final class NativeProvider extends JNIObject {
|
||||
@WrapForJNI(calledFrom = "ui")
|
||||
private void setAttached(final boolean attached) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user