--- a/accessible/src/Makefile.in
+++ b/accessible/src/Makefile.in
@@ -58,16 +58,17 @@ endif
endif
DIRS += $(PLATFORM_DIR)
DIRS += \
base \
generic \
html \
+ jsat \
xpcom \
xforms \
$(null)
ifdef MOZ_XUL
DIRS += xul
endif
--- a/accessible/src/base/Role.h
+++ b/accessible/src/base/Role.h
@@ -35,16 +35,20 @@
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
#ifndef _role_h_
#define _role_h_
+/**
+ * Note: Make sure to update the localized role names when changing the list.
+ */
+
namespace mozilla {
namespace a11y {
namespace roles {
enum Role {
/**
* Used when accessible hans't strong defined role.
*/
NOTHING = 0,
new file mode 100644
--- /dev/null
+++ b/accessible/src/jsat/AccessFu.css
@@ -0,0 +1,22 @@
+/* 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/. */
+
+#virtual-cursor-box {
+ position: fixed;
+ border: 1px solid orange;
+ pointer-events: none;
+ display: none;
+ border-radius: 2px;
+ box-shadow: 1px 1px 1px #444;
+}
+
+#virtual-cursor-inset {
+ border-radius: 1px;
+ box-shadow: inset 1px 1px 1px #444;
+ display: block;
+ box-sizing: border-box;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/accessible/src/jsat/AccessFu.jsm
@@ -0,0 +1,258 @@
+/* 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/. */
+
+'use strict';
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+var EXPORTED_SYMBOLS = ['AccessFu'];
+
+Cu.import('resource://gre/modules/Services.jsm');
+
+Cu.import('resource://gre/modules/accessibility/Presenters.jsm');
+Cu.import('resource://gre/modules/accessibility/VirtualCursorController.jsm');
+
+var AccessFu = {
+ /**
+ * Attach chrome-layer accessibility functionality to the given chrome window.
+ * If accessibility is enabled on the platform (currently Android-only), then
+ * a special accessibility mode is started (see startup()).
+ * @param {ChromeWindow} aWindow Chrome window to attach to.
+ * @param {boolean} aForceEnabled Skip platform accessibility check and enable
+ * AccessFu.
+ */
+ attach: function attach(aWindow) {
+ dump('AccessFu attach!! ' + Services.appinfo.OS + '\n');
+ this.chromeWin = aWindow;
+ this.presenters = [];
+
+ function checkA11y() {
+ if (Services.appinfo.OS == 'Android') {
+ let msg = Cc['@mozilla.org/android/bridge;1'].
+ getService(Ci.nsIAndroidBridge).handleGeckoMessage(
+ JSON.stringify(
+ { gecko: {
+ type: 'Accessibility:IsEnabled',
+ eventType: 1,
+ text: []
+ }
+ }));
+ return JSON.parse(msg).enabled;
+ }
+ return false;
+ }
+
+ if (checkA11y())
+ this.enable();
+ },
+
+ /**
+ * Start the special AccessFu mode, this primarily means controlling the virtual
+ * cursor with arrow keys. Currently, on platforms other than Android this needs
+ * to be called explicitly.
+ */
+ enable: function enable() {
+ dump('AccessFu enable');
+ this.addPresenter(new VisualPresenter());
+
+ // Implicitly add the Android presenter on Android.
+ if (Services.appinfo.OS == 'Android')
+ this.addPresenter(new AndroidPresenter());
+
+ VirtualCursorController.attach(this.chromeWin);
+
+ Services.obs.addObserver(this, 'accessible-event', false);
+ this.chromeWin.addEventListener('DOMActivate', this, true);
+ this.chromeWin.addEventListener('resize', this, true);
+ this.chromeWin.addEventListener('scroll', this, true);
+ this.chromeWin.addEventListener('TabOpen', this, true);
+ this.chromeWin.addEventListener('TabSelect', this, true);
+ this.chromeWin.addEventListener('TabClosed', this, true);
+ },
+
+ /**
+ * Disable AccessFu and return to default interaction mode.
+ */
+ disable: function disable() {
+ dump('AccessFu disable');
+
+ this.presenters.forEach(function(p) {p.detach();});
+ this.presenters = [];
+
+ VirtualCursorController.detach();
+
+ Services.obs.addObserver(this, 'accessible-event', false);
+ this.chromeWin.removeEventListener('DOMActivate', this);
+ this.chromeWin.removeEventListener('resize', this);
+ this.chromeWin.removeEventListener('scroll', this);
+ this.chromeWin.removeEventListener('TabOpen', this);
+ this.chromeWin.removeEventListener('TabSelect', this);
+ this.chromeWin.removeEventListener('TabClose', this);
+ },
+
+ addPresenter: function addPresenter(presenter) {
+ this.presenters.push(presenter);
+ presenter.attach(this.chromeWin);
+ },
+
+ handleEvent: function handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case 'TabSelect':
+ {
+ this.getDocAccessible(
+ function(docAcc) {
+ this.presenters.forEach(function(p) {p.tabSelected(docAcc);});
+ });
+ break;
+ }
+ case 'DOMActivate':
+ {
+ let activatedAcc = getAccessible(aEvent.originalTarget);
+ let state = {};
+ activatedAcc.getState(state, {});
+
+ // Checkable objects will have a state changed event that we will use
+ // instead of this hackish DOMActivate. We will also know the true
+ // action that was taken.
+ if (state.value & Ci.nsIAccessibleStates.STATE_CHECKABLE)
+ return;
+
+ this.presenters.forEach(function(p) {
+ p.actionInvoked(activatedAcc, 'click');
+ });
+ break;
+ }
+ case 'scroll':
+ case 'resize':
+ {
+ this.presenters.forEach(function(p) {p.viewportChanged();});
+ break;
+ }
+ }
+ },
+
+ getDocAccessible: function getDocAccessible(aCallback) {
+ let browserApp = (Services.appinfo.OS == 'Android') ?
+ this.chromeWin.BrowserApp : this.chromeWin.gBrowser;
+
+ let docAcc = getAccessible(browserApp.selectedBrowser.contentDocument);
+ if (!docAcc) {
+ // Wait for a reorder event fired by the parent of the new doc.
+ this._pendingDocuments[browserApp.selectedBrowser] = aCallback;
+ } else {
+ aCallback.apply(this, [docAcc]);
+ }
+ },
+
+ observe: function observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case 'accessible-event':
+ let event;
+ try {
+ event = aSubject.QueryInterface(Ci.nsIAccessibleEvent);
+ this.handleAccEvent(event);
+ } catch (ex) {
+ dump(ex);
+ return;
+ }
+ }
+ },
+
+ handleAccEvent: function handleAccEvent(aEvent) {
+ switch (aEvent.eventType) {
+ case Ci.nsIAccessibleEvent.EVENT_VIRTUALCURSOR_CHANGED:
+ {
+ let pivot = aEvent.accessible.
+ QueryInterface(Ci.nsIAccessibleCursorable).virtualCursor;
+ let event = aEvent.
+ QueryInterface(Ci.nsIAccessibleVirtualCursorChangeEvent);
+
+ let newContext = this.getNewContext(event.oldAccessible,
+ pivot.position);
+ this.presenters.forEach(
+ function(p) {
+ p.pivotChanged(pivot.position, newContext);
+ });
+ break;
+ }
+ case Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE:
+ {
+ let event = aEvent.QueryInterface(Ci.nsIAccessibleStateChangeEvent);
+ if (event.state == Ci.nsIAccessibleStates.STATE_CHECKED &&
+ !(event.isExtraState())) {
+ this.presenters.forEach(
+ function(p) {
+ p.actionInvoked(aEvent.accessible,
+ event.isEnabled() ? 'check' : 'uncheck');
+ }
+ );
+ }
+ break;
+ }
+ case Ci.nsIAccessibleEvent.EVENT_REORDER:
+ {
+ let node = aEvent.accessible.DOMNode;
+ let callback = this._pendingDocuments[node];
+ if (callback && aEvent.accessible.childCount) {
+ // We have a callback associated with a document.
+ callback.apply(this, [aEvent.accessible.getChildAt(0)]);
+ delete this._pendingDocuments[node];
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ },
+
+ getNewContext: function getNewContext(aOldObject, aNewObject) {
+ let newLineage = [];
+ let oldLineage = [];
+
+ let parent = aNewObject;
+ while ((parent = parent.parent))
+ newLineage.push(parent);
+
+ if (aOldObject) {
+ parent = aOldObject;
+ while ((parent = parent.parent))
+ oldLineage.push(parent);
+ }
+
+// newLineage.reverse();
+// oldLineage.reverse();
+
+ let i = 0;
+ let newContext = [];
+
+ while (true) {
+ let newAncestor = newLineage.pop();
+ let oldAncestor = oldLineage.pop();
+
+ if (newAncestor == undefined)
+ break;
+
+ if (newAncestor != oldAncestor)
+ newContext.push(newAncestor);
+ i++;
+ }
+
+ return newContext;
+ },
+
+ // A hash of documents that don't yet have an accessible tree.
+ _pendingDocuments: {}
+};
+
+function getAccessible(aNode) {
+ try {
+ return Cc['@mozilla.org/accessibleRetrieval;1'].
+ getService(Ci.nsIAccessibleRetrieval).getAccessibleFor(aNode);
+ } catch (e) {
+ return null;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/accessible/src/jsat/Makefile.in
@@ -0,0 +1,15 @@
+# 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/.
+
+DEPTH = ../../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+ $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/accessibility
new file mode 100644
--- /dev/null
+++ b/accessible/src/jsat/Presenters.jsm
@@ -0,0 +1,273 @@
+/* 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/. */
+
+'use strict';
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import('resource://gre/modules/accessibility/UtteranceGenerator.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+
+var EXPORTED_SYMBOLS = ['VisualPresenter',
+ 'AndroidPresenter',
+ 'DummyAndroidPresenter'];
+
+/**
+ * The interface for all presenter classes. A presenter could be, for example,
+ * a speech output module, or a visual cursor indicator.
+ */
+function Presenter() {}
+
+Presenter.prototype = {
+ /**
+ * The padding in pixels between the object and the highlight border.
+ */
+ BORDER_PADDING: 2,
+
+ /**
+ * Attach function for presenter.
+ * @param {ChromeWindow} aWindow Chrome window the presenter could use.
+ */
+ attach: function attach(aWindow) {},
+
+ /**
+ * Detach function.
+ */
+ detach: function detach() {},
+
+ /**
+ * The virtual cursor's position changed.
+ * @param {nsIAccessible} aObject the new position.
+ * @param {nsIAccessible[]} aNewContext the ancestry of the new position that
+ * is different from the old virtual cursor position.
+ */
+ pivotChanged: function pivotChanged(aObject, aNewContext) {},
+
+ /**
+ * An object's action has been invoked.
+ * @param {nsIAccessible} aObject the object that has been invoked.
+ * @param {string} aActionName the name of the action.
+ */
+ actionInvoked: function actionInvoked(aObject, aActionName) {},
+
+ /**
+ * Text has changed, either by the user or by the system. TODO.
+ */
+ textChanged: function textChanged() {},
+
+ /**
+ * Text selection has changed. TODO.
+ */
+ textSelectionChanged: function textSelectionChanged() {},
+
+ /**
+ * Selection has changed. TODO.
+ * @param {nsIAccessible} aObject the object that has been selected.
+ */
+ selectionChanged: function selectionChanged(aObject) {},
+
+ /**
+ * The page state has changed, loading, stopped loading, etc. TODO.
+ */
+ pageStateChanged: function pageStateChanged() {},
+
+ /**
+ * The tab has changed.
+ * @param {nsIAccessible} aObject the document contained in the tab.
+ */
+ tabSelected: function tabSelected(aObject) {},
+
+ /**
+ * The viewport has changed, either a scroll, pan, zoom, or
+ * landscape/portrait toggle.
+ */
+ viewportChanged: function viewportChanged() {}
+};
+
+/**
+ * Visual presenter. Draws a box around the virtual cursor's position.
+ */
+
+function VisualPresenter() {}
+
+VisualPresenter.prototype = new Presenter();
+
+VisualPresenter.prototype.attach = function(aWindow) {
+ this.chromeWin = aWindow;
+
+ // Add stylesheet
+ let stylesheetURL = 'chrome://global/content/accessibility/AccessFu.css';
+ this.stylesheet = aWindow.document.createProcessingInstruction(
+ 'xml-stylesheet', 'href="' + stylesheetURL + '" type="text/css"');
+ aWindow.document.insertBefore(this.stylesheet, aWindow.document.firstChild);
+
+ // Add highlight box
+ this.highlightBox = this.chromeWin.document.
+ createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ this.chromeWin.document.documentElement.appendChild(this.highlightBox);
+ this.highlightBox.id = 'virtual-cursor-box';
+
+ // Add highlight inset for inner shadow
+ let inset = this.chromeWin.document.
+ createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ inset.id = 'virtual-cursor-inset';
+
+ this.highlightBox.appendChild(inset);
+};
+
+VisualPresenter.prototype.detach = function() {
+ this.chromeWin.document.removeChild(this.stylesheet);
+ this.highlightBox.parentNode.removeChild(this.highlightBox);
+ this.highlightBox = this.stylesheet = null;
+};
+
+VisualPresenter.prototype.viewportChanged = function() {
+ if (this._currentObject)
+ this.highlight(this._currentObject);
+};
+
+VisualPresenter.prototype.pivotChanged = function(aObject, aNewContext) {
+ this._currentObject = aObject;
+
+ if (!aObject) {
+ this.hide();
+ return;
+ }
+
+ try {
+ aObject.scrollTo(Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE);
+ this.highlight(aObject);
+ } catch (e) {
+ dump('Error getting bounds: ' + e);
+ return;
+ }
+};
+
+VisualPresenter.prototype.tabSelected = function(aObject) {
+ let vcDoc = aObject.QueryInterface(Ci.nsIAccessibleCursorable);
+ this.pivotChanged(vcDoc.virtualCursor.position);
+};
+
+// Internals
+
+VisualPresenter.prototype.hide = function hide() {
+ this.highlightBox.style.display = 'none';
+};
+
+VisualPresenter.prototype.highlight = function(aObject) {
+ let vp = (Services.appinfo.OS == 'Android') ?
+ this.chromeWin.BrowserApp.selectedTab.getViewport() :
+ { zoom: 1.0, offsetY: 0 };
+
+ let bounds = this.getBounds(aObject, vp.zoom);
+
+ // First hide it to avoid flickering when changing the style.
+ this.highlightBox.style.display = 'none';
+ this.highlightBox.style.top = bounds.top + 'px';
+ this.highlightBox.style.left = bounds.left + 'px';
+ this.highlightBox.style.width = bounds.width + 'px';
+ this.highlightBox.style.height = bounds.height + 'px';
+ this.highlightBox.style.display = 'block';
+};
+
+VisualPresenter.prototype.getBounds = function(aObject, aZoom, aStart, aEnd) {
+ let objX = {}, objY = {}, objW = {}, objH = {};
+
+ if (aEnd >= 0 && aStart >= 0 && aEnd != aStart) {
+ // TODO: Get bounds for text ranges. Leaving this blank until we have
+ // proper text navigation in the virtual cursor.
+ }
+
+ aObject.getBounds(objX, objY, objW, objH);
+
+ // Can't specify relative coords in nsIAccessible.getBounds, so we do it.
+ let docX = {}, docY = {};
+ let docRoot = aObject.rootDocument.QueryInterface(Ci.nsIAccessible);
+ docRoot.getBounds(docX, docY, {}, {});
+
+ let rv = {
+ left: Math.round((objX.value - docX.value - this.BORDER_PADDING) * aZoom),
+ top: Math.round((objY.value - docY.value - this.BORDER_PADDING) * aZoom),
+ width: Math.round((objW.value + (this.BORDER_PADDING * 2)) * aZoom),
+ height: Math.round((objH.value + (this.BORDER_PADDING * 2)) * aZoom)
+ };
+
+ return rv;
+};
+
+/**
+ * Android presenter. Fires Android a11y events.
+ */
+
+const ANDROID_TYPE_VIEW_CLICKED = 0x01;
+const ANDROID_TYPE_VIEW_LONG_CLICKED = 0x02;
+const ANDROID_TYPE_VIEW_SELECTED = 0x04;
+const ANDROID_TYPE_VIEW_FOCUSED = 0x08;
+const ANDROID_TYPE_VIEW_TEXT_CHANGED = 0x10;
+const ANDROID_TYPE_WINDOW_STATE_CHANGED = 0x20;
+
+function AndroidPresenter() {}
+
+AndroidPresenter.prototype = new Presenter();
+
+AndroidPresenter.prototype.pivotChanged = function(aObject, aNewContext) {
+ let output = [];
+ for (let i in aNewContext)
+ output.push.apply(output,
+ UtteranceGenerator.genForObject(aNewContext[i]));
+
+ output.push.apply(output,
+ UtteranceGenerator.genForObject(aObject, true));
+
+ this.sendMessageToJava({
+ gecko: {
+ type: 'Accessibility:Event',
+ eventType: ANDROID_TYPE_VIEW_FOCUSED,
+ text: output
+ }
+ });
+};
+
+AndroidPresenter.prototype.actionInvoked = function(aObject, aActionName) {
+ this.sendMessageToJava({
+ gecko: {
+ type: 'Accessibility:Event',
+ eventType: ANDROID_TYPE_VIEW_CLICKED,
+ text: [UtteranceGenerator.genForAction(aObject, aActionName)]
+ }
+ });
+};
+
+AndroidPresenter.prototype.tabSelected = function(aObject) {
+ let vcDoc = aObject.QueryInterface(Ci.nsIAccessibleCursorable);
+ let context = [];
+
+ let parent = vcDoc.virtualCursor.position || aObject;
+ while ((parent = parent.parent))
+ context.push(parent);
+ context.reverse();
+
+ this.pivotChanged(vcDoc.virtualCursor.position, context);
+};
+
+AndroidPresenter.prototype.sendMessageToJava = function(aMessage) {
+ return Cc['@mozilla.org/android/bridge;1'].
+ getService(Ci.nsIAndroidBridge).
+ handleGeckoMessage(JSON.stringify(aMessage));
+};
+
+/**
+ * A dummy Android presenter for desktop testing
+ */
+
+function DummyAndroidPresenter() {}
+
+DummyAndroidPresenter.prototype = new AndroidPresenter();
+
+DummyAndroidPresenter.prototype.sendMessageToJava = function(aMessage) {
+ dump(JSON.stringify(aMessage, null, 2) + '\n');
+};
new file mode 100644
--- /dev/null
+++ b/accessible/src/jsat/UtteranceGenerator.jsm
@@ -0,0 +1,162 @@
+/* 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/. */
+
+'use strict';
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+const INCLUDE_ROLE = 0x01;
+const INCLUDE_NAME = 0x02;
+const INCLUDE_CUSTOM = 0x04;
+
+var gStringBundle = Cc['@mozilla.org/intl/stringbundle;1'].
+ getService(Ci.nsIStringBundleService).
+ createBundle('chrome://global/locale/AccessFu.properties');
+
+var gAccRetrieval = Cc['@mozilla.org/accessibleRetrieval;1'].
+ getService(Ci.nsIAccessibleRetrieval);
+
+var EXPORTED_SYMBOLS = ['UtteranceGenerator'];
+
+var UtteranceGenerator = {
+ gActionMap: {
+ jump: 'jumpAction',
+ press: 'pressAction',
+ check: 'checkAction',
+ uncheck: 'uncheckAction',
+ select: 'selectAction',
+ open: 'openAction',
+ close: 'closeAction',
+ switch: 'switchAction',
+ click: 'clickAction',
+ collapse: 'collapseAction',
+ expand: 'expandAction',
+ activate: 'activateAction',
+ cycle: 'cycleAction'
+ },
+
+ genForObject: function(aAccessible, aForceName) {
+ let roleString = gAccRetrieval.getStringRole(aAccessible.role);
+
+ let func = this.objectUtteranceFunctions[roleString] ||
+ this.objectUtteranceFunctions.defaultFunc;
+
+ let flags = this.verbosityRoleMap[roleString] || 0;
+
+ if (aForceName)
+ flags |= INCLUDE_NAME;
+
+ return func(aAccessible, roleString, flags);
+ },
+
+ genForAction: function(aObject, aActionName) {
+ return gStringBundle.GetStringFromName(this.gActionMap[aActionName]);
+ },
+
+ verbosityRoleMap: {
+ 'menubar': INCLUDE_ROLE,
+ 'scrollbar': INCLUDE_ROLE,
+ 'grip': INCLUDE_ROLE,
+ 'alert': INCLUDE_ROLE,
+ 'menupopup': INCLUDE_ROLE,
+ 'menuitem': INCLUDE_ROLE,
+ 'tooltip': INCLUDE_ROLE,
+ 'application': INCLUDE_NAME,
+ 'document': INCLUDE_NAME,
+ 'toolbar': INCLUDE_ROLE,
+ 'link': INCLUDE_ROLE,
+ 'list': INCLUDE_ROLE,
+ 'listitem': INCLUDE_ROLE,
+ 'outline': INCLUDE_ROLE,
+ 'outlineitem': INCLUDE_ROLE,
+ 'pagetab': INCLUDE_ROLE,
+ 'graphic': INCLUDE_ROLE | INCLUDE_NAME,
+ 'statictext': INCLUDE_NAME,
+ 'text leaf': INCLUDE_NAME,
+ 'pushbutton': INCLUDE_ROLE,
+ 'checkbutton': INCLUDE_ROLE | INCLUDE_NAME,
+ 'radiobutton': INCLUDE_ROLE | INCLUDE_NAME,
+ 'combobox': INCLUDE_ROLE,
+ 'droplist': INCLUDE_ROLE,
+ 'progressbar': INCLUDE_ROLE,
+ 'slider': INCLUDE_ROLE,
+ 'spinbutton': INCLUDE_ROLE,
+ 'diagram': INCLUDE_ROLE,
+ 'animation': INCLUDE_ROLE,
+ 'equation': INCLUDE_ROLE,
+ 'buttonmenu': INCLUDE_ROLE,
+ 'pagetablist': INCLUDE_ROLE,
+ 'canvas': INCLUDE_ROLE,
+ 'check menu item': INCLUDE_ROLE,
+ 'label': INCLUDE_ROLE,
+ 'password text': INCLUDE_ROLE,
+ 'popup menu': INCLUDE_ROLE,
+ 'radio menu item': INCLUDE_ROLE,
+ 'toggle button': INCLUDE_ROLE,
+ 'header': INCLUDE_ROLE,
+ 'footer': INCLUDE_ROLE,
+ 'entry': INCLUDE_ROLE,
+ 'caption': INCLUDE_ROLE,
+ 'document frame': INCLUDE_ROLE,
+ 'heading': INCLUDE_ROLE,
+ 'calendar': INCLUDE_ROLE,
+ 'combobox list': INCLUDE_ROLE,
+ 'combobox option': INCLUDE_ROLE,
+ 'image map': INCLUDE_ROLE,
+ 'option': INCLUDE_ROLE,
+ 'listbox': INCLUDE_ROLE},
+
+ objectUtteranceFunctions: {
+ defaultFunc: function defaultFunc(aAccessible, aRoleStr, aFlags) {
+ let name = (aFlags & INCLUDE_NAME) ? (aAccessible.name || '') : '';
+ let desc = (aFlags & INCLUDE_ROLE) ?
+ gStringBundle.GetStringFromName(aRoleStr) : '';
+
+ if (!name && !desc)
+ return [];
+
+ let state = {};
+ let extState = {};
+ aAccessible.getState(state, extState);
+
+ if (state.value & Ci.nsIAccessibleStates.STATE_CHECKABLE) {
+ let stateStr = (state.value & Ci.nsIAccessibleStates.STATE_CHECKED) ?
+ 'objChecked' : 'objNotChecked';
+ desc = gStringBundle.formatStringFromName(stateStr, [desc], 1);
+ }
+
+ if (extState.value & Ci.nsIAccessibleStates.EXT_STATE_EXPANDABLE) {
+ let stateStr = (state.value & Ci.nsIAccessibleStates.STATE_EXPANDED) ?
+ 'objExpanded' : 'objCollapsed';
+ desc = gStringBundle.formatStringFromName(stateStr, [desc], 1);
+ }
+
+ return [desc, name];
+ },
+
+ heading: function(aAccessible, aRoleStr, aFlags) {
+ let name = (aFlags & INCLUDE_NAME) ? (aAccessible.name || '') : '';
+ let level = {};
+ aAccessible.groupPosition(level, {}, {});
+ let desc = gStringBundle.formatStringFromName('headingLevel',
+ [level.value], 1);
+ return [desc, name];
+ },
+
+ listitem: function(aAccessible, aRoleStr, aFlags) {
+ let name = (aFlags & INCLUDE_NAME) ? (aAccessible.name || '') : '';
+ let localizedRole = gStringBundle.GetStringFromName(aRoleStr);
+ let itemno = {};
+ let itemof = {};
+ aAccessible.groupPosition({}, itemof, itemno);
+ let desc = gStringBundle.formatStringFromName(
+ 'objItemOf', [localizedRole, itemno.value, itemof.value], 3);
+
+ return [desc, name];
+ }
+ }
+};
new file mode 100644
--- /dev/null
+++ b/accessible/src/jsat/VirtualCursorController.jsm
@@ -0,0 +1,143 @@
+/* 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/. */
+
+'use strict';
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+var EXPORTED_SYMBOLS = ['VirtualCursorController'];
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+
+var gAccRetrieval = Cc['@mozilla.org/accessibleRetrieval;1'].
+ getService(Ci.nsIAccessibleRetrieval);
+
+var VirtualCursorController = {
+ attach: function attach(aWindow) {
+ this.chromeWin = aWindow;
+ this.chromeWin.document.addEventListener('keypress', this.onkeypress, true);
+ },
+
+ detach: function detach() {
+ this.chromeWin.document.removeEventListener('keypress', this.onkeypress);
+ },
+
+ getBrowserApp: function getBrowserApp() {
+ switch (Services.appinfo.OS) {
+ case 'Android':
+ return this.chromeWin.BrowserApp;
+ default:
+ return this.chromeWin.gBrowser;
+ }
+ },
+
+ onkeypress: function onkeypress(aEvent) {
+ let document = VirtualCursorController.getBrowserApp().
+ selectedBrowser.contentDocument;
+
+ dump('keypress ' + aEvent.keyCode + '\n');
+
+ switch (aEvent.keyCode) {
+ case aEvent.DOM_END:
+ VirtualCursorController.moveForward(document, true);
+ break;
+ case aEvent.DOM_HOME:
+ VirtualCursorController.moveBackward(document, true);
+ break;
+ case aEvent.DOM_VK_DOWN:
+ VirtualCursorController.moveForward(document, aEvent.shiftKey);
+ break;
+ case aEvent.DOM_VK_UP:
+ VirtualCursorController.moveBackward(document, aEvent.shiftKey);
+ break;
+ case aEvent.DOM_VK_RETURN:
+ //It is true that desktop does not map the kp enter key to ENTER.
+ //So for desktop we require a ctrl+return instead.
+ if (Services.appinfo.OS == 'Android' || !aEvent.ctrlKey)
+ return;
+ case aEvent.DOM_VK_ENTER:
+ VirtualCursorController.activateCurrent(document);
+ break;
+ default:
+ return;
+ }
+
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ },
+
+ moveForward: function moveForward(document, last) {
+ let virtualCursor = this.getVirtualCursor(document);
+ if (last) {
+ virtualCursor.moveLast(this.SimpleTraversalRule);
+ } else {
+ virtualCursor.moveNext(this.SimpleTraversalRule);
+ }
+ },
+
+ moveBackward: function moveBackward(document, first) {
+ let virtualCursor = this.getVirtualCursor(document);
+
+ if (first) {
+ virtualCursor.moveFirst(this.SimpleTraversalRule);
+ return
+
+ }
+
+ if (!virtualCursor.movePrevious(this.SimpleTraversalRule) &&
+ Services.appinfo.OS == 'Android') {
+ // Return focus to browser chrome, which in Android is a native widget.
+ Cc['@mozilla.org/android/bridge;1'].
+ getService(Ci.nsIAndroidBridge).handleGeckoMessage(
+ JSON.stringify({ gecko: { type: 'ToggleChrome:Focus' } }));
+ virtualCursor.position = null;
+ }
+ },
+
+ activateCurrent: function activateCurrent(document) {
+ let virtualCursor = this.getVirtualCursor(document);
+ let acc = virtualCursor.position;
+
+ if (acc.numActions > 0)
+ acc.doAction(0);
+ },
+
+ getVirtualCursor: function getVirtualCursor(document) {
+ return gAccRetrieval.getAccessibleFor(document).
+ QueryInterface(Ci.nsIAccessibleCursorable).virtualCursor;
+ },
+
+ SimpleTraversalRule: {
+ getMatchRoles: function(aRules) {
+ aRules.value = [];
+ return 0;
+ },
+
+ preFilter: Ci.nsIAccessibleTraversalRule.PREFILTER_DEFUNCT |
+ Ci.nsIAccessibleTraversalRule.PREFILTER_INVISIBLE,
+
+ match: function(aAccessible) {
+ let rv = Ci.nsIAccessibleTraversalRule.FILTER_IGNORE;
+ if (aAccessible.childCount == 0) {
+ // TODO: Find a better solution for ROLE_STATICTEXT.
+ // Right now it helps filter list bullets, but it is also used
+ // in CSS generated content.
+ let ignoreRoles = [Ci.nsIAccessibleRole.ROLE_WHITESPACE,
+ Ci.nsIAccessibleRole.ROLE_STATICTEXT];
+ let state = {};
+ aAccessible.getState(state, {});
+ if ((state.value & Ci.nsIAccessibleStates.STATE_FOCUSABLE) ||
+ (aAccessible.name && ignoreRoles.indexOf(aAccessible.role) < 0))
+ rv = Ci.nsIAccessibleTraversalRule.FILTER_MATCH;
+ }
+ return rv;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAccessibleTraversalRule])
+ }
+};
new file mode 100644
--- /dev/null
+++ b/accessible/src/jsat/jar.mn
@@ -0,0 +1,2 @@
+toolkit.jar:
+ content/global/accessibility/AccessFu.css (AccessFu.css)
new file mode 100644
--- /dev/null
+++ b/dom/locales/en-US/chrome/accessibility/AccessFu.properties
@@ -0,0 +1,91 @@
+# 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/.
+
+menubar = menu bar
+scrollbar = scroll bar
+grip = grip
+alert = alert
+menupopup = menu popup
+document = document
+pane = pane
+dialog = dialog
+separator = separator
+toolbar = toolbar
+statusbar = status bar
+table = table
+columnheader = column header
+rowheader = row header
+column = column
+row = row
+cell = cell
+link = link
+list = list
+listitem = list item
+outline = outline
+outlineitem = outline item
+pagetab = page tab
+propertypage = property page
+graphic = graphic
+pushbutton = button
+checkbutton = check button
+radiobutton = radio button
+combobox = combo box
+progressbar = progress bar
+slider = slider
+spinbutton = spin button
+diagram = diagram
+animation = animation
+equation = equation
+buttonmenu = button menu
+whitespace = white space
+pagetablist = page tab list
+canvas = canvas
+checkmenuitem = check menu item
+label = label
+passwordtext = password text
+radiomenuitem = radio menu item
+textcontainer = text container
+togglebutton = toggle button
+treetable = tree table
+header = header
+footer = footer
+paragraph = paragraph
+entry = entry
+caption = caption
+heading = heading
+section = section
+form = form
+comboboxlist = combo box list
+comboboxoption = combo box option
+imagemap = image map
+listboxoption = list box option
+listbox = list box
+flatequation = flat equation
+gridcell = gridcell
+note = note
+figure = figure
+
+# More sophisiticated object descriptions
+headingLevel = heading level %S
+# LOCALIZATION NOTE: %1$S is the item's role name (e.g. "List item" or "Page tab"), %2$S is the position of the item n the set. %3$S is the total number of such items in the set. An expanded example would read "List item 2 of 5".
+objItemOf = %1$S %2$S of %3$S
+objChecked = checked %S
+objNotChecked = not checked %S
+objExpanded = expanded %S
+objCollapsed = collapsed %S
+
+# Invoked actions
+jumpAction = jumped
+pressAction = pressed
+checkAction = checked
+uncheckAction = unchecked
+selectAction = selected
+openAction = opened
+closeAction = closed
+switchAction = switched
+clickAction = clicked
+collapseAction = collapsed
+expandAction = expanded
+activateAction = activated
+cycleAction = cycled
\ No newline at end of file
--- a/dom/locales/jar.mn
+++ b/dom/locales/jar.mn
@@ -21,8 +21,9 @@
locale/@AB_CD@/global/layout/htmlparser.properties (%chrome/layout/htmlparser.properties)
locale/@AB_CD@/global/layout/xmlparser.properties (%chrome/layout/xmlparser.properties)
locale/@AB_CD@/global/layout/HtmlForm.properties (%chrome/layout/HtmlForm.properties)
locale/@AB_CD@/global/security/caps.properties (%chrome/security/caps.properties)
locale/@AB_CD@/global/xml/prettyprint.dtd (%chrome/xml/prettyprint.dtd)
locale/@AB_CD@/global-platform/win/accessible.properties (%chrome/accessibility/win/accessible.properties)
locale/@AB_CD@/global-platform/mac/accessible.properties (%chrome/accessibility/mac/accessible.properties)
locale/@AB_CD@/global-platform/unix/accessible.properties (%chrome/accessibility/unix/accessible.properties)
+ locale/@AB_CD@/global/AccessFu.properties (%chrome/accessibility/AccessFu.properties)