mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 795957 - [PATCH 1/2][AccessFu] Adding support for live regions. r=eejay
--- accessible/src/jsat/AccessFu.jsm | 6 + accessible/src/jsat/EventManager.jsm | 192 ++++++++++++++++++-- accessible/src/jsat/OutputGenerator.jsm | 10 +- accessible/src/jsat/Presentation.jsm | 47 +++++- accessible/src/jsat/Utils.jsm | 39 +++-- .../en-US/chrome/accessibility/AccessFu.properties | 4 + 6 files changed, 263 insertions(+), 35 deletions(-)
This commit is contained in:
parent
d57e8764ad
commit
53d2a48acc
@ -163,6 +163,9 @@ this.AccessFu = {
|
||||
Services.obs.removeObserver(this, 'Accessibility:LongPress');
|
||||
Services.obs.removeObserver(this, 'Accessibility:MoveByGranularity');
|
||||
|
||||
delete this._quicknavModesPref;
|
||||
delete this._notifyOutputPref;
|
||||
|
||||
if (this.doneCallback) {
|
||||
this.doneCallback();
|
||||
delete this.doneCallback;
|
||||
@ -171,6 +174,9 @@ this.AccessFu = {
|
||||
|
||||
_enableOrDisable: function _enableOrDisable() {
|
||||
try {
|
||||
if (!this._activatePref) {
|
||||
return;
|
||||
}
|
||||
let activatePref = this._activatePref.value;
|
||||
if (activatePref == ACCESSFU_ENABLE ||
|
||||
this._systemPref && activatePref == ACCESSFU_AUTO)
|
||||
|
@ -14,10 +14,15 @@ const EVENT_TEXT_CARET_MOVED = Ci.nsIAccessibleEvent.EVENT_TEXT_CARET_MOVED;
|
||||
const EVENT_TEXT_INSERTED = Ci.nsIAccessibleEvent.EVENT_TEXT_INSERTED;
|
||||
const EVENT_TEXT_REMOVED = Ci.nsIAccessibleEvent.EVENT_TEXT_REMOVED;
|
||||
const EVENT_FOCUS = Ci.nsIAccessibleEvent.EVENT_FOCUS;
|
||||
const EVENT_SHOW = Ci.nsIAccessibleEvent.EVENT_SHOW;
|
||||
const EVENT_HIDE = Ci.nsIAccessibleEvent.EVENT_HIDE;
|
||||
|
||||
const ROLE_INTERNAL_FRAME = Ci.nsIAccessibleRole.ROLE_INTERNAL_FRAME;
|
||||
const ROLE_DOCUMENT = Ci.nsIAccessibleRole.ROLE_DOCUMENT;
|
||||
const ROLE_CHROME_WINDOW = Ci.nsIAccessibleRole.ROLE_CHROME_WINDOW;
|
||||
const ROLE_TEXT_LEAF = Ci.nsIAccessibleRole.ROLE_TEXT_LEAF;
|
||||
|
||||
const TEXT_NODE = 3;
|
||||
|
||||
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
|
||||
XPCOMUtils.defineLazyModuleGetter(this, 'Services',
|
||||
@ -140,7 +145,11 @@ this.EventManager.prototype = {
|
||||
// Don't bother with non-content events in firefox.
|
||||
if (Utils.MozBuildApp == 'browser' &&
|
||||
aEvent.eventType != EVENT_VIRTUALCURSOR_CHANGED &&
|
||||
aEvent.accessibleDocument.docType == 'window') {
|
||||
// XXX Bug 442005 results in DocAccessible::getDocType returning
|
||||
// NS_ERROR_FAILURE. Checking for aEvent.accessibleDocument.docType ==
|
||||
// 'window' does not currently work.
|
||||
(aEvent.accessibleDocument.DOMDocument.doctype &&
|
||||
aEvent.accessibleDocument.DOMDocument.doctype.name === 'window')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -219,28 +228,47 @@ this.EventManager.prototype = {
|
||||
this.editState = editState;
|
||||
break;
|
||||
}
|
||||
case EVENT_SHOW:
|
||||
{
|
||||
let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
|
||||
['additions', 'all']);
|
||||
// Only handle show if it is a relevant live region.
|
||||
if (!liveRegion) {
|
||||
break;
|
||||
}
|
||||
// Show for text is handled by the EVENT_TEXT_INSERTED handler.
|
||||
if (aEvent.accessible.role === ROLE_TEXT_LEAF) {
|
||||
break;
|
||||
}
|
||||
this._dequeueLiveEvent(EVENT_HIDE, liveRegion);
|
||||
this.present(Presentation.liveRegion(liveRegion, isPolite, false));
|
||||
break;
|
||||
}
|
||||
case EVENT_HIDE:
|
||||
{
|
||||
let {liveRegion, isPolite} = this._handleLiveRegion(
|
||||
aEvent.QueryInterface(Ci.nsIAccessibleHideEvent),
|
||||
['removals', 'all']);
|
||||
// Only handle hide if it is a relevant live region.
|
||||
if (!liveRegion) {
|
||||
break;
|
||||
}
|
||||
// Hide for text is handled by the EVENT_TEXT_REMOVED handler.
|
||||
if (aEvent.accessible.role === ROLE_TEXT_LEAF) {
|
||||
break;
|
||||
}
|
||||
this._queueLiveEvent(EVENT_HIDE, liveRegion, isPolite);
|
||||
break;
|
||||
}
|
||||
case EVENT_TEXT_INSERTED:
|
||||
case EVENT_TEXT_REMOVED:
|
||||
{
|
||||
if (aEvent.isFromUserInput) {
|
||||
// XXX support live regions as well.
|
||||
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;
|
||||
}
|
||||
this.present(Presentation.textChanged(
|
||||
isInserted, event.start, event.length,
|
||||
text, event.modifiedText));
|
||||
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;
|
||||
}
|
||||
@ -258,6 +286,130 @@ this.EventManager.prototype = {
|
||||
}
|
||||
},
|
||||
|
||||
_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, '').trim();
|
||||
if (!modifiedText) {
|
||||
return;
|
||||
}
|
||||
if (aLiveRegion) {
|
||||
if (aEvent.eventType === EVENT_TEXT_REMOVED) {
|
||||
this._queueLiveEvent(EVENT_TEXT_REMOVED, aLiveRegion, aIsPolite,
|
||||
modifiedText);
|
||||
} else {
|
||||
this._dequeueLiveEvent(EVENT_TEXT_REMOVED, aLiveRegion);
|
||||
this.present(Presentation.liveRegion(aLiveRegion, aIsPolite, false,
|
||||
modifiedText));
|
||||
}
|
||||
} else {
|
||||
this.present(Presentation.textChanged(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) {
|
||||
Utils.win.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: Utils.win.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) {
|
||||
this.sendMsgFunc("AccessFu:Present", aPresentationData);
|
||||
},
|
||||
|
@ -116,7 +116,6 @@ this.OutputGenerator = {
|
||||
let extState = {};
|
||||
aAccessible.getState(state, extState);
|
||||
let states = {base: state.value, ext: extState.value};
|
||||
|
||||
return func.apply(this, [aAccessible, roleString, states, flags, aContext]);
|
||||
},
|
||||
|
||||
@ -418,6 +417,15 @@ this.UtteranceGenerator = {
|
||||
return [gStringBundle.GetStringFromName(this.gActionMap[aActionName])];
|
||||
},
|
||||
|
||||
genForLiveRegion: function genForLiveRegion(aContext, aIsHide, aModifiedText) {
|
||||
let utterance = [];
|
||||
if (aIsHide) {
|
||||
utterance.push(gStringBundle.GetStringFromName('hidden'));
|
||||
}
|
||||
return utterance.concat(
|
||||
aModifiedText || this.genForContext(aContext).output);
|
||||
},
|
||||
|
||||
genForAnnouncement: function genForAnnouncement(aAnnouncement) {
|
||||
try {
|
||||
return [gStringBundle.GetStringFromName(aAnnouncement)];
|
||||
|
@ -102,7 +102,19 @@ Presenter.prototype = {
|
||||
/**
|
||||
* Announce something. Typically an app state change.
|
||||
*/
|
||||
announce: function announce(aAnnouncement) {}
|
||||
announce: function announce(aAnnouncement) {},
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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: function liveRegionShown(aContext, aIsPolite, aIsHide,
|
||||
aModifiedText) {}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -409,6 +421,13 @@ AndroidPresenter.prototype = {
|
||||
fromIndex: 0
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
liveRegion: function AndroidPresenter_liveRegion(aContext, aIsPolite,
|
||||
aIsHide, aModifiedText) {
|
||||
return this.announce(
|
||||
UtteranceGenerator.genForLiveRegion(aContext, aIsHide,
|
||||
aModifiedText).join(' '));
|
||||
}
|
||||
};
|
||||
|
||||
@ -451,6 +470,21 @@ SpeechPresenter.prototype = {
|
||||
]
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
liveRegion: function SpeechPresenter_liveRegion(aContext, aIsPolite, aIsHide,
|
||||
aModifiedText) {
|
||||
return {
|
||||
type: this.type,
|
||||
details: {
|
||||
actions: [{
|
||||
method: 'speak',
|
||||
data: UtteranceGenerator.genForLiveRegion(aContext, aIsHide,
|
||||
aModifiedText).join(' '),
|
||||
options: {enqueue: aIsPolite}
|
||||
}]
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@ -570,5 +604,16 @@ this.Presentation = {
|
||||
// but there really isn't a point here.
|
||||
return [p.announce(UtteranceGenerator.genForAnnouncement(aAnnouncement)[0])
|
||||
for each (p in this.presenters)];
|
||||
},
|
||||
|
||||
liveRegion: function Presentation_liveRegion(aAccessible, aIsPolite, aIsHide,
|
||||
aModifiedText) {
|
||||
let context;
|
||||
if (!aModifiedText) {
|
||||
context = new PivotContext(aAccessible, null, -1, -1, true,
|
||||
aIsHide ? true : false);
|
||||
}
|
||||
return [p.liveRegion(context, aIsPolite, aIsHide, aModifiedText) for (
|
||||
p of this.presenters)];
|
||||
}
|
||||
};
|
||||
|
@ -266,6 +266,15 @@ this.Utils = {
|
||||
return true;
|
||||
},
|
||||
|
||||
matchAttributeValue: function matchAttributeValue(aAttributeValue, values) {
|
||||
let attrSet = new Set(aAttributeValue.split(' '));
|
||||
for (let value of values) {
|
||||
if (attrSet.has(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getLandmarkName: function getLandmarkName(aAccessible) {
|
||||
const landmarks = [
|
||||
'banner',
|
||||
@ -281,11 +290,7 @@ this.Utils = {
|
||||
}
|
||||
|
||||
// Looking up a role that would match a landmark.
|
||||
for (let landmark of landmarks) {
|
||||
if (roles.indexOf(landmark) > -1) {
|
||||
return landmark;
|
||||
}
|
||||
}
|
||||
return this.matchAttributeValue(roles, landmarks);
|
||||
}
|
||||
};
|
||||
|
||||
@ -422,12 +427,15 @@ this.Logger = {
|
||||
* for a given accessible and its relationship with another accessible.
|
||||
*/
|
||||
this.PivotContext = function PivotContext(aAccessible, aOldAccessible,
|
||||
aStartOffset, aEndOffset) {
|
||||
aStartOffset, aEndOffset, aIgnoreAncestry = false,
|
||||
aIncludeInvisible = false) {
|
||||
this._accessible = aAccessible;
|
||||
this._oldAccessible =
|
||||
this._isDefunct(aOldAccessible) ? null : aOldAccessible;
|
||||
this.startOffset = aStartOffset;
|
||||
this.endOffset = aEndOffset;
|
||||
this._ignoreAncestry = aIgnoreAncestry;
|
||||
this._includeInvisible = aIncludeInvisible;
|
||||
}
|
||||
|
||||
PivotContext.prototype = {
|
||||
@ -497,7 +505,7 @@ PivotContext.prototype = {
|
||||
*/
|
||||
get oldAncestry() {
|
||||
if (!this._oldAncestry) {
|
||||
if (!this._oldAccessible) {
|
||||
if (!this._oldAccessible || this._ignoreAncestry) {
|
||||
this._oldAncestry = [];
|
||||
} else {
|
||||
this._oldAncestry = this._getAncestry(this._oldAccessible);
|
||||
@ -512,7 +520,8 @@ PivotContext.prototype = {
|
||||
*/
|
||||
get currentAncestry() {
|
||||
if (!this._currentAncestry) {
|
||||
this._currentAncestry = this._getAncestry(this._accessible);
|
||||
this._currentAncestry = this._ignoreAncestry ? [] :
|
||||
this._getAncestry(this._accessible);
|
||||
}
|
||||
return this._currentAncestry;
|
||||
},
|
||||
@ -524,7 +533,7 @@ PivotContext.prototype = {
|
||||
*/
|
||||
get newAncestry() {
|
||||
if (!this._newAncestry) {
|
||||
this._newAncestry = [currentAncestor for (
|
||||
this._newAncestry = this._ignoreAncestry ? [] : [currentAncestor for (
|
||||
[index, currentAncestor] of Iterator(this.currentAncestry)) if (
|
||||
currentAncestor !== this.oldAncestry[index])];
|
||||
}
|
||||
@ -543,9 +552,14 @@ PivotContext.prototype = {
|
||||
}
|
||||
let child = aAccessible.firstChild;
|
||||
while (child) {
|
||||
let state = {};
|
||||
child.getState(state, {});
|
||||
if (!(state.value & Ci.nsIAccessibleStates.STATE_INVISIBLE)) {
|
||||
let include;
|
||||
if (this._includeInvisible) {
|
||||
include = true;
|
||||
} else {
|
||||
let [state,] = Utils.getStates(child);
|
||||
include = !(state.value & Ci.nsIAccessibleStates.STATE_INVISIBLE);
|
||||
}
|
||||
if (include) {
|
||||
if (aPreorder) {
|
||||
yield child;
|
||||
[yield node for (node of this._traverse(child, aPreorder, aStop))];
|
||||
@ -703,7 +717,6 @@ PrefCache.prototype = {
|
||||
if (!this.type) {
|
||||
this.type = aBranch.getPrefType(this.name);
|
||||
}
|
||||
|
||||
switch (this.type) {
|
||||
case Ci.nsIPrefBranch.PREF_STRING:
|
||||
return aBranch.getCharPref(this.name);
|
||||
|
@ -129,6 +129,10 @@ expandAction = expanded
|
||||
activateAction = activated
|
||||
cycleAction = cycled
|
||||
|
||||
# Live regions
|
||||
# 'hidden' will be spoken when something disappears in a live region.
|
||||
hidden = hidden
|
||||
|
||||
# Tab states
|
||||
tabLoading = loading
|
||||
tabLoaded = loaded
|
||||
|
Loading…
Reference in New Issue
Block a user