2012-04-13 16:18:57 -07:00
|
|
|
/* 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');
|
|
|
|
|
2012-05-03 19:28:35 -07:00
|
|
|
const ACCESSFU_DISABLE = 0;
|
|
|
|
const ACCESSFU_ENABLE = 1;
|
|
|
|
const ACCESSFU_AUTO = 2;
|
|
|
|
|
2012-04-13 16:18:57 -07:00
|
|
|
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) {
|
2012-05-03 19:28:35 -07:00
|
|
|
if (this.chromeWin)
|
|
|
|
// XXX: only supports attaching to one window now.
|
|
|
|
throw new Error('Only one window could be attached to AccessFu');
|
|
|
|
|
2012-04-13 16:18:57 -07:00
|
|
|
dump('AccessFu attach!! ' + Services.appinfo.OS + '\n');
|
|
|
|
this.chromeWin = aWindow;
|
|
|
|
this.presenters = [];
|
|
|
|
|
2012-05-03 19:28:35 -07:00
|
|
|
this.prefsBranch = Cc['@mozilla.org/preferences-service;1']
|
|
|
|
.getService(Ci.nsIPrefService).getBranch('accessibility.');
|
|
|
|
this.prefsBranch.addObserver('accessfu', this, false);
|
|
|
|
|
|
|
|
let accessPref = ACCESSFU_DISABLE;
|
|
|
|
try {
|
|
|
|
accessPref = this.prefsBranch.getIntPref('accessfu');
|
|
|
|
} catch (x) {
|
2012-04-13 16:18:57 -07:00
|
|
|
}
|
|
|
|
|
2012-05-03 19:28:35 -07:00
|
|
|
if (this.amINeeded(accessPref))
|
2012-04-13 16:18:57 -07:00
|
|
|
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);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Disable AccessFu and return to default interaction mode.
|
|
|
|
*/
|
|
|
|
disable: function disable() {
|
|
|
|
dump('AccessFu disable');
|
|
|
|
|
2012-05-07 09:44:44 -07:00
|
|
|
this.presenters.forEach(function(p) { p.detach(); });
|
2012-04-13 16:18:57 -07:00
|
|
|
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);
|
|
|
|
},
|
|
|
|
|
2012-05-03 19:28:35 -07:00
|
|
|
amINeeded: function(aPref) {
|
|
|
|
switch (aPref) {
|
|
|
|
case ACCESSFU_ENABLE:
|
|
|
|
return true;
|
|
|
|
case ACCESSFU_AUTO:
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2012-04-13 16:18:57 -07:00
|
|
|
addPresenter: function addPresenter(presenter) {
|
|
|
|
this.presenters.push(presenter);
|
|
|
|
presenter.attach(this.chromeWin);
|
|
|
|
},
|
|
|
|
|
|
|
|
handleEvent: function handleEvent(aEvent) {
|
|
|
|
switch (aEvent.type) {
|
2012-05-07 09:44:44 -07:00
|
|
|
case 'TabOpen':
|
|
|
|
{
|
|
|
|
let browser = aEvent.target.linkedBrowser || aEvent.target;
|
|
|
|
// Store the new browser node. We will need to check later when a new
|
|
|
|
// content document is attached if it has been attached to this new tab.
|
|
|
|
// If it has, than we will need to send a 'loading' message along with
|
|
|
|
// the usual 'newdoc' to presenters.
|
|
|
|
this._pendingDocuments[browser] = true;
|
|
|
|
this.presenters.forEach(function(p) { p.tabStateChanged(null, 'newtab'); });
|
|
|
|
break;
|
|
|
|
}
|
2012-04-13 16:18:57 -07:00
|
|
|
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':
|
|
|
|
{
|
2012-05-07 09:44:44 -07:00
|
|
|
this.presenters.forEach(function(p) { p.viewportChanged(); });
|
2012-04-13 16:18:57 -07:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
observe: function observe(aSubject, aTopic, aData) {
|
|
|
|
switch (aTopic) {
|
2012-05-03 19:28:35 -07:00
|
|
|
case 'nsPref:changed':
|
|
|
|
if (aData == 'accessfu') {
|
|
|
|
if (this.amINeeded(this.prefsBranch.getIntPref('accessfu')))
|
|
|
|
this.enable();
|
|
|
|
else
|
|
|
|
this.disable();
|
|
|
|
}
|
|
|
|
break;
|
2012-04-13 16:18:57 -07:00
|
|
|
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');
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
2012-05-07 09:44:44 -07:00
|
|
|
else if (event.state == Ci.nsIAccessibleStates.STATE_BUSY &&
|
|
|
|
!(event.isExtraState()) && event.isEnabled()) {
|
|
|
|
let role = event.accessible.role;
|
|
|
|
if ((role == Ci.nsIAccessibleRole.ROLE_DOCUMENT ||
|
|
|
|
role == Ci.nsIAccessibleRole.ROLE_APPLICATION)) {
|
|
|
|
// An existing document has changed to state "busy", this means
|
|
|
|
// something is loading. Send a 'loading' message to presenters.
|
|
|
|
this.presenters.forEach(
|
|
|
|
function(p) {
|
|
|
|
p.tabStateChanged(event.accessible, 'loading');
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2012-04-13 16:18:57 -07:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case Ci.nsIAccessibleEvent.EVENT_REORDER:
|
|
|
|
{
|
2012-05-07 09:44:44 -07:00
|
|
|
let acc = aEvent.accessible;
|
|
|
|
if (acc.childCount) {
|
|
|
|
let docAcc = acc.getChildAt(0);
|
|
|
|
if (this._pendingDocuments[aEvent.DOMNode]) {
|
|
|
|
// This is a document in a new tab. Check if it is
|
|
|
|
// in a BUSY state (i.e. loading), and inform presenters.
|
|
|
|
// We need to do this because a state change event will not be
|
|
|
|
// fired when an object is created with the BUSY state.
|
|
|
|
// If this is not a new tab, don't bother because we sent 'loading'
|
|
|
|
// when the previous doc changed its state to BUSY.
|
|
|
|
let state = {};
|
|
|
|
docAcc.getState(state, {});
|
|
|
|
if (state.value & Ci.nsIAccessibleStates.STATE_BUSY &&
|
|
|
|
this.isNotChromeDoc(docAcc))
|
|
|
|
this.presenters.forEach(
|
|
|
|
function(p) { p.tabStateChanged(docAcc, 'loading'); }
|
|
|
|
);
|
|
|
|
delete this._pendingDocuments[aEvent.DOMNode];
|
|
|
|
}
|
|
|
|
if (this.isBrowserDoc(docAcc))
|
|
|
|
// A new top-level content document has been attached
|
|
|
|
this.presenters.forEach(
|
|
|
|
function(p) { p.tabStateChanged(docAcc, 'newdoc'); }
|
|
|
|
);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case Ci.nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE:
|
|
|
|
{
|
|
|
|
if (this.isNotChromeDoc(aEvent.accessible)) {
|
|
|
|
this.presenters.forEach(
|
|
|
|
function(p) {
|
|
|
|
p.tabStateChanged(aEvent.accessible, 'loaded');
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case Ci.nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_STOPPED:
|
|
|
|
{
|
|
|
|
this.presenters.forEach(
|
|
|
|
function(p) {
|
|
|
|
p.tabStateChanged(aEvent.accessible, 'loadstopped');
|
|
|
|
}
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case Ci.nsIAccessibleEvent.EVENT_DOCUMENT_RELOAD:
|
|
|
|
{
|
|
|
|
this.presenters.forEach(
|
|
|
|
function(p) {
|
|
|
|
p.tabStateChanged(aEvent.accessible, 'reload');
|
|
|
|
}
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case Ci.nsIAccessibleEvent.EVENT_FOCUS:
|
|
|
|
{
|
|
|
|
if (this.isBrowserDoc(aEvent.accessible)) {
|
|
|
|
// The document recieved focus, call tabSelected to present current tab.
|
|
|
|
this.presenters.forEach(
|
|
|
|
function(p) { p.tabSelected(aEvent.accessible); });
|
2012-04-13 16:18:57 -07:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2012-05-02 22:10:55 -07:00
|
|
|
case Ci.nsIAccessibleEvent.EVENT_TEXT_INSERTED:
|
|
|
|
case Ci.nsIAccessibleEvent.EVENT_TEXT_REMOVED:
|
|
|
|
{
|
|
|
|
if (aEvent.isFromUserInput) {
|
|
|
|
// XXX support live regions as well.
|
|
|
|
let event = aEvent.QueryInterface(Ci.nsIAccessibleTextChangeEvent);
|
|
|
|
let isInserted = event.isInserted();
|
|
|
|
let textIface = aEvent.accessible.QueryInterface(Ci.nsIAccessibleText);
|
|
|
|
|
|
|
|
let text = '';
|
|
|
|
try {
|
|
|
|
text = textIface.
|
|
|
|
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 (textIface.characterCount)
|
|
|
|
throw x;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.presenters.forEach(
|
|
|
|
function(p) {
|
|
|
|
p.textChanged(isInserted, event.start, event.length, text, event.modifiedText);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2012-04-13 16:18:57 -07:00
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2012-05-07 09:44:44 -07:00
|
|
|
/**
|
|
|
|
* Check if accessible is a top-level content document (i.e. a child of a XUL
|
|
|
|
* browser node).
|
|
|
|
* @param {nsIAccessible} aDocAcc the accessible to check.
|
|
|
|
* @return {boolean} true if this is a top-level content document.
|
|
|
|
*/
|
|
|
|
isBrowserDoc: function isBrowserDoc(aDocAcc) {
|
|
|
|
let parent = aDocAcc.parent;
|
|
|
|
if (!parent)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
let domNode = parent.DOMNode;
|
|
|
|
if (!domNode)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
const ns = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
|
|
|
|
return (domNode.localName == 'browser' && domNode.namespaceURI == ns);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if document is not a local "chrome" document, like about:home.
|
|
|
|
* @param {nsIDOMDocument} aDocument the document to check.
|
|
|
|
* @return {boolean} true if this is not a chrome document.
|
|
|
|
*/
|
|
|
|
isNotChromeDoc: function isNotChromeDoc(aDocument) {
|
|
|
|
let location = aDocument.DOMNode.location;
|
|
|
|
if (!location)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
return location.protocol != "about:";
|
|
|
|
},
|
|
|
|
|
2012-04-13 16:18:57 -07:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|