|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 'use strict'; |
|
6 |
|
7 const Cc = Components.classes; |
|
8 const Ci = Components.interfaces; |
|
9 const Cu = Components.utils; |
|
10 const Cr = Components.results; |
|
11 |
|
12 const INCLUDE_DESC = 0x01; |
|
13 const INCLUDE_NAME = 0x02; |
|
14 const INCLUDE_VALUE = 0x04; |
|
15 const INCLUDE_CUSTOM = 0x08; |
|
16 const NAME_FROM_SUBTREE_RULE = 0x10; |
|
17 const IGNORE_EXPLICIT_NAME = 0x20; |
|
18 |
|
19 const OUTPUT_DESC_FIRST = 0; |
|
20 const OUTPUT_DESC_LAST = 1; |
|
21 |
|
22 Cu.import('resource://gre/modules/XPCOMUtils.jsm'); |
|
23 XPCOMUtils.defineLazyModuleGetter(this, 'Utils', |
|
24 'resource://gre/modules/accessibility/Utils.jsm'); |
|
25 XPCOMUtils.defineLazyModuleGetter(this, 'PrefCache', |
|
26 'resource://gre/modules/accessibility/Utils.jsm'); |
|
27 XPCOMUtils.defineLazyModuleGetter(this, 'Logger', |
|
28 'resource://gre/modules/accessibility/Utils.jsm'); |
|
29 XPCOMUtils.defineLazyModuleGetter(this, 'PluralForm', |
|
30 'resource://gre/modules/PluralForm.jsm'); |
|
31 XPCOMUtils.defineLazyModuleGetter(this, 'Roles', |
|
32 'resource://gre/modules/accessibility/Constants.jsm'); |
|
33 XPCOMUtils.defineLazyModuleGetter(this, 'States', |
|
34 'resource://gre/modules/accessibility/Constants.jsm'); |
|
35 |
|
36 this.EXPORTED_SYMBOLS = ['UtteranceGenerator', 'BrailleGenerator']; |
|
37 |
|
38 this.OutputGenerator = { |
|
39 |
|
40 defaultOutputOrder: OUTPUT_DESC_LAST, |
|
41 |
|
42 /** |
|
43 * Generates output for a PivotContext. |
|
44 * @param {PivotContext} aContext object that generates and caches |
|
45 * context information for a given accessible and its relationship with |
|
46 * another accessible. |
|
47 * @return {Object} An object that neccessarily has an output property which |
|
48 * is an array of strings. Depending on the utterance order, |
|
49 * the strings describe the context for an accessible object either |
|
50 * starting from the accessible's ancestry or accessible's subtree. |
|
51 * The object may also have properties specific to the type of output |
|
52 * generated. |
|
53 */ |
|
54 genForContext: function genForContext(aContext) { |
|
55 let output = []; |
|
56 let self = this; |
|
57 let addOutput = function addOutput(aAccessible) { |
|
58 output.push.apply(output, self.genForObject(aAccessible, aContext)); |
|
59 }; |
|
60 let ignoreSubtree = function ignoreSubtree(aAccessible) { |
|
61 let roleString = Utils.AccRetrieval.getStringRole(aAccessible.role); |
|
62 let nameRule = self.roleRuleMap[roleString] || 0; |
|
63 // Ignore subtree if the name is explicit and the role's name rule is the |
|
64 // NAME_FROM_SUBTREE_RULE. |
|
65 return (((nameRule & INCLUDE_VALUE) && aAccessible.value) || |
|
66 ((nameRule & NAME_FROM_SUBTREE_RULE) && |
|
67 (Utils.getAttributes(aAccessible)['explicit-name'] === 'true' && |
|
68 !(nameRule & IGNORE_EXPLICIT_NAME)))); |
|
69 }; |
|
70 |
|
71 let contextStart = this._getContextStart(aContext); |
|
72 |
|
73 if (this.outputOrder === OUTPUT_DESC_FIRST) { |
|
74 contextStart.forEach(addOutput); |
|
75 addOutput(aContext.accessible); |
|
76 [addOutput(node) for |
|
77 (node of aContext.subtreeGenerator(true, ignoreSubtree))]; |
|
78 } else { |
|
79 [addOutput(node) for |
|
80 (node of aContext.subtreeGenerator(false, ignoreSubtree))]; |
|
81 addOutput(aContext.accessible); |
|
82 contextStart.reverse().forEach(addOutput); |
|
83 } |
|
84 |
|
85 // Clean up the white space. |
|
86 let trimmed; |
|
87 output = [trimmed for (word of output) if (trimmed = word.trim())]; |
|
88 return {output: output}; |
|
89 }, |
|
90 |
|
91 |
|
92 /** |
|
93 * Generates output for an object. |
|
94 * @param {nsIAccessible} aAccessible accessible object to generate output |
|
95 * for. |
|
96 * @param {PivotContext} aContext object that generates and caches |
|
97 * context information for a given accessible and its relationship with |
|
98 * another accessible. |
|
99 * @return {Array} Two string array. The first string describes the object |
|
100 * and its state. The second string is the object's name. Whether the |
|
101 * object's description or it's role is included is determined by |
|
102 * {@link roleRuleMap}. |
|
103 */ |
|
104 genForObject: function genForObject(aAccessible, aContext) { |
|
105 let roleString = Utils.AccRetrieval.getStringRole(aAccessible.role); |
|
106 let func = this.objectOutputFunctions[ |
|
107 OutputGenerator._getOutputName(roleString)] || |
|
108 this.objectOutputFunctions.defaultFunc; |
|
109 |
|
110 let flags = this.roleRuleMap[roleString] || 0; |
|
111 |
|
112 if (aAccessible.childCount == 0) |
|
113 flags |= INCLUDE_NAME; |
|
114 |
|
115 return func.apply(this, [aAccessible, roleString, |
|
116 Utils.getState(aAccessible), flags, aContext]); |
|
117 }, |
|
118 |
|
119 /** |
|
120 * Generates output for an action performed. |
|
121 * @param {nsIAccessible} aAccessible accessible object that the action was |
|
122 * invoked in. |
|
123 * @param {string} aActionName the name of the action, one of the keys in |
|
124 * {@link gActionMap}. |
|
125 * @return {Array} A one string array with the action. |
|
126 */ |
|
127 genForAction: function genForAction(aObject, aActionName) {}, |
|
128 |
|
129 /** |
|
130 * Generates output for an announcement. Basically attempts to localize |
|
131 * the announcement string. |
|
132 * @param {string} aAnnouncement unlocalized announcement. |
|
133 * @return {Array} A one string array with the announcement. |
|
134 */ |
|
135 genForAnnouncement: function genForAnnouncement(aAnnouncement) {}, |
|
136 |
|
137 /** |
|
138 * Generates output for a tab state change. |
|
139 * @param {nsIAccessible} aAccessible accessible object of the tab's attached |
|
140 * document. |
|
141 * @param {string} aTabState the tab state name, see |
|
142 * {@link Presenter.tabStateChanged}. |
|
143 * @return {Array} The tab state utterace. |
|
144 */ |
|
145 genForTabStateChange: function genForTabStateChange(aObject, aTabState) {}, |
|
146 |
|
147 /** |
|
148 * Generates output for announcing entering and leaving editing mode. |
|
149 * @param {aIsEditing} boolean true if we are in editing mode |
|
150 * @return {Array} The mode utterance |
|
151 */ |
|
152 genForEditingMode: function genForEditingMode(aIsEditing) {}, |
|
153 |
|
154 _getContextStart: function getContextStart(aContext) {}, |
|
155 |
|
156 _addName: function _addName(aOutput, aAccessible, aFlags) { |
|
157 let name; |
|
158 if ((Utils.getAttributes(aAccessible)['explicit-name'] === 'true' && |
|
159 !(aFlags & IGNORE_EXPLICIT_NAME)) || (aFlags & INCLUDE_NAME)) { |
|
160 name = aAccessible.name; |
|
161 } |
|
162 |
|
163 let description = aAccessible.description; |
|
164 if (description) { |
|
165 // Compare against the calculated name unconditionally, regardless of name rule, |
|
166 // so we can make sure we don't speak duplicated descriptions |
|
167 let tmpName = name || aAccessible.name; |
|
168 if (tmpName && (description !== tmpName)) { |
|
169 name = name || ''; |
|
170 name = this.outputOrder === OUTPUT_DESC_FIRST ? |
|
171 description + ' - ' + name : |
|
172 name + ' - ' + description; |
|
173 } |
|
174 } |
|
175 |
|
176 if (name) { |
|
177 aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? |
|
178 'push' : 'unshift'](name); |
|
179 } |
|
180 }, |
|
181 |
|
182 /** |
|
183 * Adds a landmark role to the output if available. |
|
184 * @param {Array} aOutput Output array. |
|
185 * @param {nsIAccessible} aAccessible current accessible object. |
|
186 */ |
|
187 _addLandmark: function _addLandmark(aOutput, aAccessible) { |
|
188 let landmarkName = Utils.getLandmarkName(aAccessible); |
|
189 if (!landmarkName) { |
|
190 return; |
|
191 } |
|
192 |
|
193 let landmark = Utils.stringBundle.GetStringFromName(landmarkName); |
|
194 if (!landmark) { |
|
195 return; |
|
196 } |
|
197 |
|
198 aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? 'unshift' : 'push']( |
|
199 landmark); |
|
200 }, |
|
201 |
|
202 /** |
|
203 * Adds an entry type attribute to the description if available. |
|
204 * @param {Array} aDesc Description array. |
|
205 * @param {nsIAccessible} aAccessible current accessible object. |
|
206 * @param {String} aRoleStr aAccessible's role string. |
|
207 */ |
|
208 _addType: function _addType(aDesc, aAccessible, aRoleStr) { |
|
209 if (aRoleStr !== 'entry') { |
|
210 return; |
|
211 } |
|
212 |
|
213 let typeName = Utils.getAttributes(aAccessible)['text-input-type']; |
|
214 // Ignore the the input type="text" case. |
|
215 if (!typeName || typeName === 'text') { |
|
216 return; |
|
217 } |
|
218 typeName = 'textInputType_' + typeName; |
|
219 try { |
|
220 aDesc.push(Utils.stringBundle.GetStringFromName(typeName)); |
|
221 } catch (x) { |
|
222 Logger.warning('Failed to get a string from a bundle for', typeName); |
|
223 } |
|
224 }, |
|
225 |
|
226 get outputOrder() { |
|
227 if (!this._utteranceOrder) { |
|
228 this._utteranceOrder = new PrefCache('accessibility.accessfu.utterance'); |
|
229 } |
|
230 return typeof this._utteranceOrder.value === 'number' ? |
|
231 this._utteranceOrder.value : this.defaultOutputOrder; |
|
232 }, |
|
233 |
|
234 _getOutputName: function _getOutputName(aName) { |
|
235 return aName.replace(' ', ''); |
|
236 }, |
|
237 |
|
238 _getLocalizedRole: function _getLocalizedRole(aRoleStr) {}, |
|
239 |
|
240 _getLocalizedState: function _getLocalizedState(aState) {}, |
|
241 |
|
242 _getPluralFormString: function _getPluralFormString(aString, aCount) { |
|
243 let str = Utils.stringBundle.GetStringFromName(this._getOutputName(aString)); |
|
244 str = PluralForm.get(aCount, str); |
|
245 return str.replace('#1', aCount); |
|
246 }, |
|
247 |
|
248 roleRuleMap: { |
|
249 'menubar': INCLUDE_DESC, |
|
250 'scrollbar': INCLUDE_DESC, |
|
251 'grip': INCLUDE_DESC, |
|
252 'alert': INCLUDE_DESC | INCLUDE_NAME, |
|
253 'menupopup': INCLUDE_DESC, |
|
254 'menuitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, |
|
255 'tooltip': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, |
|
256 'columnheader': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, |
|
257 'rowheader': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, |
|
258 'column': NAME_FROM_SUBTREE_RULE, |
|
259 'row': NAME_FROM_SUBTREE_RULE, |
|
260 'cell': INCLUDE_DESC | INCLUDE_NAME, |
|
261 'application': INCLUDE_NAME, |
|
262 'document': INCLUDE_NAME, |
|
263 'grouping': INCLUDE_DESC | INCLUDE_NAME, |
|
264 'toolbar': INCLUDE_DESC, |
|
265 'table': INCLUDE_DESC | INCLUDE_NAME, |
|
266 'link': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, |
|
267 'helpballoon': NAME_FROM_SUBTREE_RULE, |
|
268 'list': INCLUDE_DESC | INCLUDE_NAME, |
|
269 'listitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, |
|
270 'outline': INCLUDE_DESC, |
|
271 'outlineitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, |
|
272 'pagetab': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, |
|
273 'graphic': INCLUDE_DESC, |
|
274 'pushbutton': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, |
|
275 'checkbutton': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, |
|
276 'radiobutton': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, |
|
277 'buttondropdown': NAME_FROM_SUBTREE_RULE, |
|
278 'combobox': INCLUDE_DESC, |
|
279 'droplist': INCLUDE_DESC, |
|
280 'progressbar': INCLUDE_DESC | INCLUDE_VALUE, |
|
281 'slider': INCLUDE_DESC | INCLUDE_VALUE, |
|
282 'spinbutton': INCLUDE_DESC | INCLUDE_VALUE, |
|
283 'diagram': INCLUDE_DESC, |
|
284 'animation': INCLUDE_DESC, |
|
285 'equation': INCLUDE_DESC, |
|
286 'buttonmenu': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, |
|
287 'buttondropdowngrid': NAME_FROM_SUBTREE_RULE, |
|
288 'pagetablist': INCLUDE_DESC, |
|
289 'canvas': INCLUDE_DESC, |
|
290 'check menu item': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, |
|
291 'label': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, |
|
292 'password text': INCLUDE_DESC, |
|
293 'popup menu': INCLUDE_DESC, |
|
294 'radio menu item': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, |
|
295 'table column header': NAME_FROM_SUBTREE_RULE, |
|
296 'table row header': NAME_FROM_SUBTREE_RULE, |
|
297 'tear off menu item': NAME_FROM_SUBTREE_RULE, |
|
298 'toggle button': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, |
|
299 'parent menuitem': NAME_FROM_SUBTREE_RULE, |
|
300 'header': INCLUDE_DESC, |
|
301 'footer': INCLUDE_DESC, |
|
302 'entry': INCLUDE_DESC | INCLUDE_NAME | INCLUDE_VALUE, |
|
303 'caption': INCLUDE_DESC, |
|
304 'document frame': INCLUDE_DESC, |
|
305 'heading': INCLUDE_DESC, |
|
306 'calendar': INCLUDE_DESC | INCLUDE_NAME, |
|
307 'combobox list': INCLUDE_DESC, |
|
308 'combobox option': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, |
|
309 'listbox option': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, |
|
310 'listbox rich option': NAME_FROM_SUBTREE_RULE, |
|
311 'gridcell': NAME_FROM_SUBTREE_RULE, |
|
312 'check rich option': NAME_FROM_SUBTREE_RULE, |
|
313 'term': NAME_FROM_SUBTREE_RULE, |
|
314 'definition': NAME_FROM_SUBTREE_RULE, |
|
315 'key': NAME_FROM_SUBTREE_RULE, |
|
316 'image map': INCLUDE_DESC, |
|
317 'option': INCLUDE_DESC, |
|
318 'listbox': INCLUDE_DESC, |
|
319 'definitionlist': INCLUDE_DESC | INCLUDE_NAME, |
|
320 'dialog': INCLUDE_DESC | INCLUDE_NAME, |
|
321 'chrome window': IGNORE_EXPLICIT_NAME, |
|
322 'app root': IGNORE_EXPLICIT_NAME }, |
|
323 |
|
324 objectOutputFunctions: { |
|
325 _generateBaseOutput: function _generateBaseOutput(aAccessible, aRoleStr, aState, aFlags) { |
|
326 let output = []; |
|
327 |
|
328 if (aFlags & INCLUDE_DESC) { |
|
329 let desc = this._getLocalizedState(aState); |
|
330 let roleStr = this._getLocalizedRole(aRoleStr); |
|
331 if (roleStr) { |
|
332 this._addType(desc, aAccessible, aRoleStr); |
|
333 desc.push(roleStr); |
|
334 } |
|
335 output.push(desc.join(' ')); |
|
336 } |
|
337 |
|
338 if (aFlags & INCLUDE_VALUE) { |
|
339 let value = aAccessible.value; |
|
340 if (value) { |
|
341 output[this.outputOrder === OUTPUT_DESC_FIRST ? |
|
342 'push' : 'unshift'](value); |
|
343 } |
|
344 } |
|
345 |
|
346 this._addName(output, aAccessible, aFlags); |
|
347 this._addLandmark(output, aAccessible); |
|
348 |
|
349 return output; |
|
350 }, |
|
351 |
|
352 label: function label(aAccessible, aRoleStr, aState, aFlags, aContext) { |
|
353 if (aContext.isNestedControl || |
|
354 aContext.accessible == Utils.getEmbeddedControl(aAccessible)) { |
|
355 // If we are on a nested control, or a nesting label, |
|
356 // we don't need the context. |
|
357 return []; |
|
358 } |
|
359 |
|
360 return this.objectOutputFunctions.defaultFunc.apply(this, arguments); |
|
361 }, |
|
362 |
|
363 entry: function entry(aAccessible, aRoleStr, aState, aFlags) { |
|
364 let rolestr = aState.contains(States.MULTI_LINE) ? 'textarea' : 'entry'; |
|
365 return this.objectOutputFunctions.defaultFunc.apply( |
|
366 this, [aAccessible, rolestr, aState, aFlags]); |
|
367 }, |
|
368 |
|
369 pagetab: function pagetab(aAccessible, aRoleStr, aState, aFlags) { |
|
370 let localizedRole = this._getLocalizedRole(aRoleStr); |
|
371 let itemno = {}; |
|
372 let itemof = {}; |
|
373 aAccessible.groupPosition({}, itemof, itemno); |
|
374 let output = []; |
|
375 let desc = this._getLocalizedState(aState); |
|
376 desc.push( |
|
377 Utils.stringBundle.formatStringFromName( |
|
378 'objItemOf', [localizedRole, itemno.value, itemof.value], 3)); |
|
379 output.push(desc.join(' ')); |
|
380 |
|
381 this._addName(output, aAccessible, aFlags); |
|
382 this._addLandmark(output, aAccessible); |
|
383 |
|
384 return output; |
|
385 }, |
|
386 |
|
387 table: function table(aAccessible, aRoleStr, aState, aFlags) { |
|
388 let output = []; |
|
389 let table; |
|
390 try { |
|
391 table = aAccessible.QueryInterface(Ci.nsIAccessibleTable); |
|
392 } catch (x) { |
|
393 Logger.logException(x); |
|
394 return output; |
|
395 } finally { |
|
396 // Check if it's a layout table, and bail out if true. |
|
397 // We don't want to speak any table information for layout tables. |
|
398 if (table.isProbablyForLayout()) { |
|
399 return output; |
|
400 } |
|
401 let tableColumnInfo = this._getPluralFormString('tableColumnInfo', |
|
402 table.columnCount); |
|
403 let tableRowInfo = this._getPluralFormString('tableRowInfo', |
|
404 table.rowCount); |
|
405 output.push(Utils.stringBundle.formatStringFromName( |
|
406 this._getOutputName('tableInfo'), [this._getLocalizedRole(aRoleStr), |
|
407 tableColumnInfo, tableRowInfo], 3)); |
|
408 this._addName(output, aAccessible, aFlags); |
|
409 this._addLandmark(output, aAccessible); |
|
410 return output; |
|
411 } |
|
412 } |
|
413 } |
|
414 }; |
|
415 |
|
416 /** |
|
417 * Generates speech utterances from objects, actions and state changes. |
|
418 * An utterance is an array of strings. |
|
419 * |
|
420 * It should not be assumed that flattening an utterance array would create a |
|
421 * gramatically correct sentence. For example, {@link genForObject} might |
|
422 * return: ['graphic', 'Welcome to my home page']. |
|
423 * Each string element in an utterance should be gramatically correct in itself. |
|
424 * Another example from {@link genForObject}: ['list item 2 of 5', 'Alabama']. |
|
425 * |
|
426 * An utterance is ordered from the least to the most important. Speaking the |
|
427 * last string usually makes sense, but speaking the first often won't. |
|
428 * For example {@link genForAction} might return ['button', 'clicked'] for a |
|
429 * clicked event. Speaking only 'clicked' makes sense. Speaking 'button' does |
|
430 * not. |
|
431 */ |
|
432 this.UtteranceGenerator = { |
|
433 __proto__: OutputGenerator, |
|
434 |
|
435 gActionMap: { |
|
436 jump: 'jumpAction', |
|
437 press: 'pressAction', |
|
438 check: 'checkAction', |
|
439 uncheck: 'uncheckAction', |
|
440 select: 'selectAction', |
|
441 unselect: 'unselectAction', |
|
442 open: 'openAction', |
|
443 close: 'closeAction', |
|
444 switch: 'switchAction', |
|
445 click: 'clickAction', |
|
446 collapse: 'collapseAction', |
|
447 expand: 'expandAction', |
|
448 activate: 'activateAction', |
|
449 cycle: 'cycleAction' |
|
450 }, |
|
451 |
|
452 //TODO: May become more verbose in the future. |
|
453 genForAction: function genForAction(aObject, aActionName) { |
|
454 return [Utils.stringBundle.GetStringFromName(this.gActionMap[aActionName])]; |
|
455 }, |
|
456 |
|
457 genForLiveRegion: function genForLiveRegion(aContext, aIsHide, aModifiedText) { |
|
458 let utterance = []; |
|
459 if (aIsHide) { |
|
460 utterance.push(Utils.stringBundle.GetStringFromName('hidden')); |
|
461 } |
|
462 return utterance.concat( |
|
463 aModifiedText || this.genForContext(aContext).output); |
|
464 }, |
|
465 |
|
466 genForAnnouncement: function genForAnnouncement(aAnnouncement) { |
|
467 try { |
|
468 return [Utils.stringBundle.GetStringFromName(aAnnouncement)]; |
|
469 } catch (x) { |
|
470 return [aAnnouncement]; |
|
471 } |
|
472 }, |
|
473 |
|
474 genForTabStateChange: function genForTabStateChange(aObject, aTabState) { |
|
475 switch (aTabState) { |
|
476 case 'newtab': |
|
477 return [Utils.stringBundle.GetStringFromName('tabNew')]; |
|
478 case 'loading': |
|
479 return [Utils.stringBundle.GetStringFromName('tabLoading')]; |
|
480 case 'loaded': |
|
481 return [aObject.name || '', |
|
482 Utils.stringBundle.GetStringFromName('tabLoaded')]; |
|
483 case 'loadstopped': |
|
484 return [Utils.stringBundle.GetStringFromName('tabLoadStopped')]; |
|
485 case 'reload': |
|
486 return [Utils.stringBundle.GetStringFromName('tabReload')]; |
|
487 default: |
|
488 return []; |
|
489 } |
|
490 }, |
|
491 |
|
492 genForEditingMode: function genForEditingMode(aIsEditing) { |
|
493 return [Utils.stringBundle.GetStringFromName( |
|
494 aIsEditing ? 'editingMode' : 'navigationMode')]; |
|
495 }, |
|
496 |
|
497 objectOutputFunctions: { |
|
498 |
|
499 __proto__: OutputGenerator.objectOutputFunctions, |
|
500 |
|
501 defaultFunc: function defaultFunc(aAccessible, aRoleStr, aState, aFlags) { |
|
502 return this.objectOutputFunctions._generateBaseOutput.apply(this, arguments); |
|
503 }, |
|
504 |
|
505 heading: function heading(aAccessible, aRoleStr, aState, aFlags) { |
|
506 let level = {}; |
|
507 aAccessible.groupPosition(level, {}, {}); |
|
508 let utterance = |
|
509 [Utils.stringBundle.formatStringFromName( |
|
510 'headingLevel', [level.value], 1)]; |
|
511 |
|
512 this._addName(utterance, aAccessible, aFlags); |
|
513 this._addLandmark(utterance, aAccessible); |
|
514 |
|
515 return utterance; |
|
516 }, |
|
517 |
|
518 listitem: function listitem(aAccessible, aRoleStr, aState, aFlags) { |
|
519 let itemno = {}; |
|
520 let itemof = {}; |
|
521 aAccessible.groupPosition({}, itemof, itemno); |
|
522 let utterance = []; |
|
523 if (itemno.value == 1) // Start of list |
|
524 utterance.push(Utils.stringBundle.GetStringFromName('listStart')); |
|
525 else if (itemno.value == itemof.value) // last item |
|
526 utterance.push(Utils.stringBundle.GetStringFromName('listEnd')); |
|
527 |
|
528 this._addName(utterance, aAccessible, aFlags); |
|
529 this._addLandmark(utterance, aAccessible); |
|
530 |
|
531 return utterance; |
|
532 }, |
|
533 |
|
534 list: function list(aAccessible, aRoleStr, aState, aFlags) { |
|
535 return this._getListUtterance |
|
536 (aAccessible, aRoleStr, aFlags, aAccessible.childCount); |
|
537 }, |
|
538 |
|
539 definitionlist: function definitionlist(aAccessible, aRoleStr, aState, aFlags) { |
|
540 return this._getListUtterance |
|
541 (aAccessible, aRoleStr, aFlags, aAccessible.childCount / 2); |
|
542 }, |
|
543 |
|
544 application: function application(aAccessible, aRoleStr, aState, aFlags) { |
|
545 // Don't utter location of applications, it gets tiring. |
|
546 if (aAccessible.name != aAccessible.DOMNode.location) |
|
547 return this.objectOutputFunctions.defaultFunc.apply(this, |
|
548 [aAccessible, aRoleStr, aState, aFlags]); |
|
549 |
|
550 return []; |
|
551 }, |
|
552 |
|
553 cell: function cell(aAccessible, aRoleStr, aState, aFlags, aContext) { |
|
554 let utterance = []; |
|
555 let cell = aContext.getCellInfo(aAccessible); |
|
556 if (cell) { |
|
557 let desc = []; |
|
558 let addCellChanged = function addCellChanged(aDesc, aChanged, aString, aIndex) { |
|
559 if (aChanged) { |
|
560 aDesc.push(Utils.stringBundle.formatStringFromName(aString, |
|
561 [aIndex + 1], 1)); |
|
562 } |
|
563 }; |
|
564 let addExtent = function addExtent(aDesc, aExtent, aString) { |
|
565 if (aExtent > 1) { |
|
566 aDesc.push(Utils.stringBundle.formatStringFromName(aString, |
|
567 [aExtent], 1)); |
|
568 } |
|
569 }; |
|
570 let addHeaders = function addHeaders(aDesc, aHeaders) { |
|
571 if (aHeaders.length > 0) { |
|
572 aDesc.push.apply(aDesc, aHeaders); |
|
573 } |
|
574 }; |
|
575 |
|
576 addCellChanged(desc, cell.columnChanged, 'columnInfo', cell.columnIndex); |
|
577 addCellChanged(desc, cell.rowChanged, 'rowInfo', cell.rowIndex); |
|
578 |
|
579 addExtent(desc, cell.columnExtent, 'spansColumns'); |
|
580 addExtent(desc, cell.rowExtent, 'spansRows'); |
|
581 |
|
582 addHeaders(desc, cell.columnHeaders); |
|
583 addHeaders(desc, cell.rowHeaders); |
|
584 |
|
585 utterance.push(desc.join(' ')); |
|
586 } |
|
587 |
|
588 this._addName(utterance, aAccessible, aFlags); |
|
589 this._addLandmark(utterance, aAccessible); |
|
590 |
|
591 return utterance; |
|
592 }, |
|
593 |
|
594 columnheader: function columnheader() { |
|
595 return this.objectOutputFunctions.cell.apply(this, arguments); |
|
596 }, |
|
597 |
|
598 rowheader: function rowheader() { |
|
599 return this.objectOutputFunctions.cell.apply(this, arguments); |
|
600 } |
|
601 }, |
|
602 |
|
603 _getContextStart: function _getContextStart(aContext) { |
|
604 return aContext.newAncestry; |
|
605 }, |
|
606 |
|
607 _getLocalizedRole: function _getLocalizedRole(aRoleStr) { |
|
608 try { |
|
609 return Utils.stringBundle.GetStringFromName( |
|
610 this._getOutputName(aRoleStr)); |
|
611 } catch (x) { |
|
612 return ''; |
|
613 } |
|
614 }, |
|
615 |
|
616 _getLocalizedState: function _getLocalizedState(aState) { |
|
617 let stateUtterances = []; |
|
618 |
|
619 if (aState.contains(States.UNAVAILABLE)) { |
|
620 stateUtterances.push( |
|
621 Utils.stringBundle.GetStringFromName('stateUnavailable')); |
|
622 } |
|
623 |
|
624 // Don't utter this in Jelly Bean, we let TalkBack do it for us there. |
|
625 // This is because we expose the checked information on the node itself. |
|
626 // XXX: this means the checked state is always appended to the end, regardless |
|
627 // of the utterance ordering preference. |
|
628 if ((Utils.AndroidSdkVersion < 16 || Utils.MozBuildApp === 'browser') && |
|
629 aState.contains(States.CHECKABLE)) { |
|
630 let statetr = aState.contains(States.CHECKED) ? |
|
631 'stateChecked' : 'stateNotChecked'; |
|
632 stateUtterances.push(Utils.stringBundle.GetStringFromName(statetr)); |
|
633 } |
|
634 |
|
635 if (aState.contains(States.PRESSED)) { |
|
636 stateUtterances.push( |
|
637 Utils.stringBundle.GetStringFromName('statePressed')); |
|
638 } |
|
639 |
|
640 if (aState.contains(States.EXPANDABLE)) { |
|
641 let statetr = aState.contains(States.EXPANDED) ? |
|
642 'stateExpanded' : 'stateCollapsed'; |
|
643 stateUtterances.push(Utils.stringBundle.GetStringFromName(statetr)); |
|
644 } |
|
645 |
|
646 if (aState.contains(States.REQUIRED)) { |
|
647 stateUtterances.push( |
|
648 Utils.stringBundle.GetStringFromName('stateRequired')); |
|
649 } |
|
650 |
|
651 if (aState.contains(States.TRAVERSED)) { |
|
652 stateUtterances.push( |
|
653 Utils.stringBundle.GetStringFromName('stateTraversed')); |
|
654 } |
|
655 |
|
656 if (aState.contains(States.HASPOPUP)) { |
|
657 stateUtterances.push( |
|
658 Utils.stringBundle.GetStringFromName('stateHasPopup')); |
|
659 } |
|
660 |
|
661 if (aState.contains(States.SELECTED)) { |
|
662 stateUtterances.push( |
|
663 Utils.stringBundle.GetStringFromName('stateSelected')); |
|
664 } |
|
665 |
|
666 return stateUtterances; |
|
667 }, |
|
668 |
|
669 _getListUtterance: function _getListUtterance(aAccessible, aRoleStr, aFlags, aItemCount) { |
|
670 let desc = []; |
|
671 let roleStr = this._getLocalizedRole(aRoleStr); |
|
672 if (roleStr) { |
|
673 desc.push(roleStr); |
|
674 } |
|
675 desc.push(this._getPluralFormString('listItemsCount', aItemCount)); |
|
676 let utterance = [desc.join(' ')]; |
|
677 |
|
678 this._addName(utterance, aAccessible, aFlags); |
|
679 this._addLandmark(utterance, aAccessible); |
|
680 |
|
681 return utterance; |
|
682 } |
|
683 }; |
|
684 |
|
685 |
|
686 this.BrailleGenerator = { |
|
687 __proto__: OutputGenerator, |
|
688 |
|
689 genForContext: function genForContext(aContext) { |
|
690 let output = OutputGenerator.genForContext.apply(this, arguments); |
|
691 |
|
692 let acc = aContext.accessible; |
|
693 |
|
694 // add the static text indicating a list item; do this for both listitems or |
|
695 // direct first children of listitems, because these are both common browsing |
|
696 // scenarios |
|
697 let addListitemIndicator = function addListitemIndicator(indicator = '*') { |
|
698 output.output.unshift(indicator); |
|
699 }; |
|
700 |
|
701 if (acc.indexInParent === 1 && |
|
702 acc.parent.role == Roles.LISTITEM && |
|
703 acc.previousSibling.role == Roles.STATICTEXT) { |
|
704 if (acc.parent.parent && acc.parent.parent.DOMNode && |
|
705 acc.parent.parent.DOMNode.nodeName == 'UL') { |
|
706 addListitemIndicator(); |
|
707 } else { |
|
708 addListitemIndicator(acc.previousSibling.name.trim()); |
|
709 } |
|
710 } else if (acc.role == Roles.LISTITEM && acc.firstChild && |
|
711 acc.firstChild.role == Roles.STATICTEXT) { |
|
712 if (acc.parent.DOMNode.nodeName == 'UL') { |
|
713 addListitemIndicator(); |
|
714 } else { |
|
715 addListitemIndicator(acc.firstChild.name.trim()); |
|
716 } |
|
717 } |
|
718 |
|
719 if (acc instanceof Ci.nsIAccessibleText) { |
|
720 output.endOffset = this.outputOrder === OUTPUT_DESC_FIRST ? |
|
721 output.output.join(' ').length : acc.characterCount; |
|
722 output.startOffset = output.endOffset - acc.characterCount; |
|
723 } |
|
724 |
|
725 return output; |
|
726 }, |
|
727 |
|
728 objectOutputFunctions: { |
|
729 |
|
730 __proto__: OutputGenerator.objectOutputFunctions, |
|
731 |
|
732 defaultFunc: function defaultFunc(aAccessible, aRoleStr, aState, aFlags) { |
|
733 return this.objectOutputFunctions._generateBaseOutput.apply(this, arguments); |
|
734 }, |
|
735 |
|
736 listitem: function listitem(aAccessible, aRoleStr, aState, aFlags) { |
|
737 let braille = []; |
|
738 |
|
739 this._addName(braille, aAccessible, aFlags); |
|
740 this._addLandmark(braille, aAccessible); |
|
741 |
|
742 return braille; |
|
743 }, |
|
744 |
|
745 cell: function cell(aAccessible, aRoleStr, aState, aFlags, aContext) { |
|
746 let braille = []; |
|
747 let cell = aContext.getCellInfo(aAccessible); |
|
748 if (cell) { |
|
749 let desc = []; |
|
750 let addHeaders = function addHeaders(aDesc, aHeaders) { |
|
751 if (aHeaders.length > 0) { |
|
752 aDesc.push.apply(aDesc, aHeaders); |
|
753 } |
|
754 }; |
|
755 |
|
756 desc.push(Utils.stringBundle.formatStringFromName( |
|
757 this._getOutputName('cellInfo'), [cell.columnIndex + 1, |
|
758 cell.rowIndex + 1], 2)); |
|
759 |
|
760 addHeaders(desc, cell.columnHeaders); |
|
761 addHeaders(desc, cell.rowHeaders); |
|
762 braille.push(desc.join(' ')); |
|
763 } |
|
764 |
|
765 this._addName(braille, aAccessible, aFlags); |
|
766 this._addLandmark(braille, aAccessible); |
|
767 return braille; |
|
768 }, |
|
769 |
|
770 columnheader: function columnheader() { |
|
771 return this.objectOutputFunctions.cell.apply(this, arguments); |
|
772 }, |
|
773 |
|
774 rowheader: function rowheader() { |
|
775 return this.objectOutputFunctions.cell.apply(this, arguments); |
|
776 }, |
|
777 |
|
778 statictext: function statictext(aAccessible, aRoleStr, aState, aFlags) { |
|
779 // Since we customize the list bullet's output, we add the static |
|
780 // text from the first node in each listitem, so skip it here. |
|
781 if (aAccessible.parent.role == Roles.LISTITEM) { |
|
782 return []; |
|
783 } |
|
784 |
|
785 return this.objectOutputFunctions._useStateNotRole.apply(this, arguments); |
|
786 }, |
|
787 |
|
788 _useStateNotRole: function _useStateNotRole(aAccessible, aRoleStr, aState, aFlags) { |
|
789 let braille = []; |
|
790 |
|
791 let desc = this._getLocalizedState(aState, aAccessible.role); |
|
792 braille.push(desc.join(' ')); |
|
793 |
|
794 this._addName(braille, aAccessible, aFlags); |
|
795 this._addLandmark(braille, aAccessible); |
|
796 |
|
797 return braille; |
|
798 }, |
|
799 |
|
800 checkbutton: function checkbutton(aAccessible, aRoleStr, aState, aFlags) { |
|
801 return this.objectOutputFunctions._useStateNotRole.apply(this, arguments); |
|
802 }, |
|
803 |
|
804 radiobutton: function radiobutton(aAccessible, aRoleStr, aState, aFlags) { |
|
805 return this.objectOutputFunctions._useStateNotRole.apply(this, arguments); |
|
806 }, |
|
807 |
|
808 togglebutton: function radiobutton(aAccessible, aRoleStr, aState, aFlags) { |
|
809 return this.objectOutputFunctions._useStateNotRole.apply(this, arguments); |
|
810 } |
|
811 }, |
|
812 |
|
813 _getContextStart: function _getContextStart(aContext) { |
|
814 if (aContext.accessible.parent.role == Roles.LINK) { |
|
815 return [aContext.accessible.parent]; |
|
816 } |
|
817 |
|
818 return []; |
|
819 }, |
|
820 |
|
821 _getOutputName: function _getOutputName(aName) { |
|
822 return OutputGenerator._getOutputName(aName) + 'Abbr'; |
|
823 }, |
|
824 |
|
825 _getLocalizedRole: function _getLocalizedRole(aRoleStr) { |
|
826 try { |
|
827 return Utils.stringBundle.GetStringFromName( |
|
828 this._getOutputName(aRoleStr)); |
|
829 } catch (x) { |
|
830 try { |
|
831 return Utils.stringBundle.GetStringFromName( |
|
832 OutputGenerator._getOutputName(aRoleStr)); |
|
833 } catch (y) { |
|
834 return ''; |
|
835 } |
|
836 } |
|
837 }, |
|
838 |
|
839 _getLocalizedState: function _getLocalizedState(aState, aRole) { |
|
840 let stateBraille = []; |
|
841 |
|
842 let getResultMarker = function getResultMarker(aMarker) { |
|
843 // aMarker is a simple boolean. |
|
844 let resultMarker = []; |
|
845 resultMarker.push('('); |
|
846 resultMarker.push(aMarker ? 'x' : ' '); |
|
847 resultMarker.push(')'); |
|
848 |
|
849 return resultMarker.join(''); |
|
850 }; |
|
851 |
|
852 if (aState.contains(States.CHECKABLE)) { |
|
853 stateBraille.push(getResultMarker(aState.contains(States.CHECKED))); |
|
854 } |
|
855 |
|
856 if (aRole === Roles.TOGGLE_BUTTON) { |
|
857 stateBraille.push(getResultMarker(aState.contains(States.PRESSED))); |
|
858 } |
|
859 |
|
860 return stateBraille; |
|
861 } |
|
862 |
|
863 }; |