Bug 830748 - [PATCH 1/2] [AccessFu] Improved reading of table semantics. r=eeejay

This commit is contained in:
Yura Zenevich 2013-06-27 14:15:36 -07:00
parent 310f9b591a
commit 95d1b39b68
3 changed files with 301 additions and 44 deletions

View File

@ -22,6 +22,11 @@ XPCOMUtils.defineLazyModuleGetter(this, 'Utils',
'resource://gre/modules/accessibility/Utils.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'PrefCache',
'resource://gre/modules/accessibility/Utils.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'Logger',
'resource://gre/modules/accessibility/Utils.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'PluralForm',
'resource://gre/modules/PluralForm.jsm');
let gUtteranceOrder = new PrefCache('accessibility.accessfu.utterance');
@ -46,7 +51,7 @@ this.OutputGenerator = {
let output = [];
let self = this;
let addOutput = function addOutput(aAccessible) {
output.push.apply(output, self.genForObject(aAccessible));
output.push.apply(output, self.genForObject(aAccessible, aContext));
};
let ignoreSubtree = function ignoreSubtree(aAccessible) {
let roleString = Utils.AccRetrieval.getStringRole(aAccessible.role);
@ -83,14 +88,18 @@ this.OutputGenerator = {
* Generates output for an object.
* @param {nsIAccessible} aAccessible accessible object to generate utterance
* for.
* @param {PivotContext} aContext object that generates and caches
* context information for a given accessible and its relationship with
* another accessible.
* @return {Array} Two string array. The first string describes the object
* and its states. The second string 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) {
genForObject: function genForObject(aAccessible, aContext) {
let roleString = Utils.AccRetrieval.getStringRole(aAccessible.role);
let func = this.objectOutputFunctions[roleString.replace(' ', '')] ||
let func = this.objectOutputFunctions[
OutputGenerator._getOutputName(roleString)] ||
this.objectOutputFunctions.defaultFunc;
let flags = this.roleRuleMap[roleString] || 0;
@ -103,7 +112,7 @@ this.OutputGenerator = {
aAccessible.getState(state, extState);
let states = {base: state.value, ext: extState.value};
return func.apply(this, [aAccessible, roleString, states, flags]);
return func.apply(this, [aAccessible, roleString, states, flags, aContext]);
},
/**
@ -158,10 +167,20 @@ this.OutputGenerator = {
}
},
_getOutputName: function _getOutputName(aName) {
return aName.replace(' ', '');
},
_getLocalizedRole: function _getLocalizedRole(aRoleStr) {},
_getLocalizedStates: function _getLocalizedStates(aStates) {},
_getPluralFormString: function _getPluralFormString(aString, aCount) {
let str = gStringBundle.GetStringFromName(this._getOutputName(aString));
str = PluralForm.get(aCount, str);
return str.replace('#1', aCount);
},
roleRuleMap: {
'menubar': INCLUDE_DESC,
'scrollbar': INCLUDE_DESC,
@ -170,10 +189,11 @@ this.OutputGenerator = {
'menupopup': INCLUDE_DESC,
'menuitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
'tooltip': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
'columnheader': NAME_FROM_SUBTREE_RULE,
'rowheader': 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,
'grouping': INCLUDE_DESC | INCLUDE_NAME,
@ -263,6 +283,32 @@ this.OutputGenerator = {
this._addName(output, aAccessible, aFlags);
return output;
},
table: function table(aAccessible, aRoleStr, aStates, 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;
}
let tableColumnInfo = this._getPluralFormString('tableColumnInfo',
table.columnCount);
let tableRowInfo = this._getPluralFormString('tableRowInfo',
table.rowCount);
output.push(gStringBundle.formatStringFromName(
this._getOutputName('tableInfo'), [this._getLocalizedRole(aRoleStr),
tableColumnInfo, tableRowInfo], 3));
this._addName(output, aAccessible, aFlags);
return output;
}
}
}
};
@ -341,12 +387,11 @@ this.UtteranceGenerator = {
},
objectOutputFunctions: {
defaultFunc: function defaultFunc(aAccessible, aRoleStr, aStates, aFlags) {
return OutputGenerator.objectOutputFunctions._generateBaseOutput.apply(this, arguments);
},
entry: function entry(aAccessible, aRoleStr, aStates, aFlags) {
return OutputGenerator.objectOutputFunctions.entry.apply(this, arguments);
__proto__: OutputGenerator.objectOutputFunctions,
defaultFunc: function defaultFunc(aAccessible, aRoleStr, aStates, aFlags) {
return this.objectOutputFunctions._generateBaseOutput.apply(this, arguments);
},
heading: function heading(aAccessible, aRoleStr, aStates, aFlags) {
@ -392,6 +437,53 @@ this.UtteranceGenerator = {
[aAccessible, aRoleStr, aStates, aFlags]);
return [];
},
cell: function cell(aAccessible, aRoleStr, aStates, aFlags, aContext) {
let utterance = [];
let cell = aContext.getCellInfo(aAccessible);
if (cell) {
let desc = [];
let addCellChanged = function addCellChanged(aDesc, aChanged, aString, aIndex) {
if (aChanged) {
aDesc.push(gStringBundle.formatStringFromName(aString,
[aIndex + 1], 1));
}
};
let addExtent = function addExtent(aDesc, aExtent, aString) {
if (aExtent > 1) {
aDesc.push(gStringBundle.formatStringFromName(aString,
[aExtent], 1));
}
};
let addHeaders = function addHeaders(aDesc, aHeaders) {
if (aHeaders.length > 0) {
aDesc.push.apply(aDesc, aHeaders);
}
};
addCellChanged(desc, cell.columnChanged, 'columnInfo', cell.columnIndex);
addCellChanged(desc, cell.rowChanged, 'rowInfo', cell.rowIndex);
addExtent(desc, cell.columnExtent, 'spansColumns');
addExtent(desc, cell.rowExtent, 'spansRows');
addHeaders(desc, cell.columnHeaders);
addHeaders(desc, cell.rowHeaders);
utterance.push(desc.join(' '));
}
this._addName(utterance, aAccessible, aFlags);
return utterance;
},
columnheader: function columnheader() {
return this.objectOutputFunctions.cell.apply(this, arguments);
},
rowheader: function rowheader() {
return this.objectOutputFunctions.cell.apply(this, arguments);
}
},
@ -401,7 +493,7 @@ this.UtteranceGenerator = {
_getLocalizedRole: function _getLocalizedRole(aRoleStr) {
try {
return gStringBundle.GetStringFromName(aRoleStr.replace(' ', ''));
return gStringBundle.GetStringFromName(this._getOutputName(aRoleStr));
} catch (x) {
return '';
}
@ -467,8 +559,11 @@ this.BrailleGenerator = {
defaultOutputOrder: OUTPUT_DESC_LAST,
objectOutputFunctions: {
__proto__: OutputGenerator.objectOutputFunctions,
defaultFunc: function defaultFunc(aAccessible, aRoleStr, aStates, aFlags) {
let braille = OutputGenerator.objectOutputFunctions._generateBaseOutput.apply(this, arguments);
let braille = this.objectOutputFunctions._generateBaseOutput.apply(this, arguments);
if (aAccessible.indexInParent === 1 &&
aAccessible.parent.role == Ci.nsIAccessibleRole.ROLE_LISTITEM &&
@ -492,6 +587,38 @@ this.BrailleGenerator = {
return braille;
},
cell: function cell(aAccessible, aRoleStr, aStates, aFlags, aContext) {
let braille = [];
let cell = aContext.getCellInfo(aAccessible);
if (cell) {
let desc = [];
let addHeaders = function addHeaders(aDesc, aHeaders) {
if (aHeaders.length > 0) {
aDesc.push.apply(aDesc, aHeaders);
}
};
desc.push(gStringBundle.formatStringFromName(
this._getOutputName('cellInfo'), [cell.columnIndex + 1,
cell.rowIndex + 1], 2));
addHeaders(desc, cell.columnHeaders);
addHeaders(desc, cell.rowHeaders);
braille.push(desc.join(' '));
}
this._addName(braille, aAccessible, aFlags);
return braille;
},
columnheader: function columnheader() {
return this.objectOutputFunctions.cell.apply(this, arguments);
},
rowheader: function rowheader() {
return this.objectOutputFunctions.cell.apply(this, arguments);
},
statictext: function statictext(aAccessible, aRoleStr, aStates, aFlags) {
// Since we customize the list bullet's output, we add the static
// text from the first node in each listitem, so skip it here.
@ -523,10 +650,6 @@ this.BrailleGenerator = {
togglebutton: function radiobutton(aAccessible, aRoleStr, aStates, aFlags) {
return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
},
entry: function entry(aAccessible, aRoleStr, aStates, aFlags) {
return OutputGenerator.objectOutputFunctions.entry.apply(this, arguments);
}
},
@ -538,12 +661,17 @@ this.BrailleGenerator = {
return [];
},
_getOutputName: function _getOutputName(aName) {
return OutputGenerator._getOutputName(aName) + 'Abbr';
},
_getLocalizedRole: function _getLocalizedRole(aRoleStr) {
try {
return gStringBundle.GetStringFromName(aRoleStr.replace(' ', '') + 'Abbr');
return gStringBundle.GetStringFromName(this._getOutputName(aRoleStr));
} catch (x) {
try {
return gStringBundle.GetStringFromName(aRoleStr.replace(' ', ''));
return gStringBundle.GetStringFromName(
OutputGenerator._getOutputName(aRoleStr));
} catch (y) {
return '';
}

View File

@ -350,6 +350,45 @@ PivotContext.prototype = {
return this._oldAccessible;
},
/**
* Get a list of |aAccessible|'s ancestry up to the root.
* @param {nsIAccessible} aAccessible.
* @return {Array} Ancestry list.
*/
_getAncestry: function _getAncestry(aAccessible) {
let ancestry = [];
let parent = aAccessible;
while (parent && (parent = parent.parent)) {
ancestry.push(parent);
}
return ancestry.reverse();
},
/**
* A list of the old accessible's ancestry.
*/
get oldAncestry() {
if (!this._oldAncestry) {
if (!this._oldAccessible) {
this._oldAncestry = [];
} else {
this._oldAncestry = this._getAncestry(this._oldAccessible);
this._oldAncestry.push(this._oldAccessible);
}
}
return this._oldAncestry;
},
/**
* A list of the current accessible's ancestry.
*/
get currentAncestry() {
if (!this._currentAncestry) {
this._currentAncestry = this._getAncestry(this._accessible);
}
return this._currentAncestry;
},
/*
* This is a list of the accessible's ancestry up to the common ancestor
* of the accessible and the old accessible. It is useful for giving the
@ -357,32 +396,10 @@ PivotContext.prototype = {
*/
get newAncestry() {
if (!this._newAncestry) {
let newLineage = [];
let oldLineage = [];
let parent = this._accessible;
while (parent && (parent = parent.parent))
newLineage.push(parent);
parent = this._oldAccessible;
while (parent && (parent = parent.parent))
oldLineage.push(parent);
this._newAncestry = [];
while (true) {
let newAncestor = newLineage.pop();
let oldAncestor = oldLineage.pop();
if (newAncestor == undefined)
break;
if (newAncestor != oldAncestor)
this._newAncestry.push(newAncestor);
}
this._newAncestry = [currentAncestor for (
[index, currentAncestor] of Iterator(this.currentAncestry)) if (
currentAncestor !== this.oldAncestry[index])];
}
return this._newAncestry;
},
@ -426,6 +443,99 @@ PivotContext.prototype = {
return this._traverse(this._accessible, aPreorder, aStop);
},
getCellInfo: function getCellInfo(aAccessible) {
if (!this._cells) {
this._cells = new WeakMap();
}
let domNode = aAccessible.DOMNode;
if (this._cells.has(domNode)) {
return this._cells.get(domNode);
}
let cellInfo = {};
let getAccessibleCell = function getAccessibleCell(aAccessible) {
if (!aAccessible) {
return null;
}
if ([Ci.nsIAccessibleRole.ROLE_CELL,
Ci.nsIAccessibleRole.ROLE_COLUMNHEADER,
Ci.nsIAccessibleRole.ROLE_ROWHEADER].indexOf(
aAccessible.role) < 0) {
return null;
}
try {
return aAccessible.QueryInterface(Ci.nsIAccessibleTableCell);
} catch (x) {
Logger.logException(x);
return null;
}
};
let getHeaders = function getHeaders(aHeaderCells) {
let enumerator = aHeaderCells.enumerate();
while (enumerator.hasMoreElements()) {
yield enumerator.getNext().QueryInterface(Ci.nsIAccessible).name;
}
};
cellInfo.current = getAccessibleCell(aAccessible);
if (!cellInfo.current) {
Logger.warning(aAccessible,
'does not support nsIAccessibleTableCell interface.');
this._cells.set(domNode, null);
return null;
}
let table = cellInfo.current.table;
if (table.isProbablyForLayout()) {
this._cells.set(domNode, null);
return null;
}
cellInfo.previous = null;
let oldAncestry = this.oldAncestry.reverse();
let ancestor = oldAncestry.shift();
while (!cellInfo.previous && ancestor) {
let cell = getAccessibleCell(ancestor);
if (cell && cell.table === table) {
cellInfo.previous = cell;
}
ancestor = oldAncestry.shift();
}
if (cellInfo.previous) {
cellInfo.rowChanged = cellInfo.current.rowIndex !==
cellInfo.previous.rowIndex;
cellInfo.columnChanged = cellInfo.current.columnIndex !==
cellInfo.previous.columnIndex;
} else {
cellInfo.rowChanged = true;
cellInfo.columnChanged = true;
}
cellInfo.rowExtent = cellInfo.current.rowExtent;
cellInfo.columnExtent = cellInfo.current.columnExtent;
cellInfo.columnIndex = cellInfo.current.columnIndex;
cellInfo.rowIndex = cellInfo.current.rowIndex;
cellInfo.columnHeaders = [];
if (cellInfo.columnChanged && cellInfo.current.role !==
Ci.nsIAccessibleRole.ROLE_COLUMNHEADER) {
cellInfo.columnHeaders = [headers for (headers of getHeaders(
cellInfo.current.columnHeaderCells))];
}
cellInfo.rowHeaders = [];
if (cellInfo.rowChanged && cellInfo.current.role ===
Ci.nsIAccessibleRole.ROLE_CELL) {
cellInfo.rowHeaders = [headers for (headers of getHeaders(
cellInfo.current.rowHeaderCells))];
}
this._cells.set(domNode, cellInfo);
return cellInfo;
},
get bounds() {
if (!this._bounds) {
let objX = {}, objY = {}, objW = {}, objH = {};

View File

@ -80,6 +80,20 @@ listStart = First item
listEnd = Last item
listItemCount = %S items
# Description of a table or grid:
# 1 is a dynamically retrieved localized role of either 'table' or 'grid'.
# 2 is the number of columns within the table.
# 3 is the number of rows within the table or grid.
tableInfo = %S with %S and %S
tableColumnInfo = 1 column;#1 columns
tableRowInfo = 1 row;#1 rows
# table or grid cell information
columnInfo = Column %S
rowInfo = Row %S
spansColumns = spans %S columns
spansRows = spans %S rows
# Invoked actions
jumpAction = jumped
pressAction = pressed
@ -141,3 +155,8 @@ passwordtextAbbr = passwdtxt
imagemapAbbr = imgmap
figureAbbr = fig
textareaAbbr = txtarea
tableAbbr = tbl
tableInfoAbbr = %S %S %S
tableColumnInfoAbbr = #1c;#1c
tableRowInfoAbbr = #1r;#1r
cellInfoAbbr = c%Sr%S