diff --git a/accessible/src/jsat/AccessFu.jsm b/accessible/src/jsat/AccessFu.jsm index c2a07158d6bf..240d0be6fcca 100644 --- a/accessible/src/jsat/AccessFu.jsm +++ b/accessible/src/jsat/AccessFu.jsm @@ -12,10 +12,9 @@ const Cr = Components.results; var EXPORTED_SYMBOLS = ['AccessFu']; Cu.import('resource://gre/modules/Services.jsm'); +Cu.import('resource://gre/modules/Geometry.jsm'); Cu.import('resource://gre/modules/accessibility/Utils.jsm'); -Cu.import('resource://gre/modules/accessibility/Presenters.jsm'); -Cu.import('resource://gre/modules/accessibility/VirtualCursorController.jsm'); Cu.import('resource://gre/modules/accessibility/TouchAdapter.jsm'); const ACCESSFU_DISABLE = 0; @@ -24,12 +23,9 @@ const ACCESSFU_AUTO = 2; 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. + * Initialize chrome-layer accessibility functionality. + * If accessibility is enabled on the platform, then a special accessibility + * mode is started. */ attach: function attach(aWindow) { if (this.chromeWin) @@ -38,21 +34,19 @@ var AccessFu = { Logger.info('attach'); this.chromeWin = aWindow; - this.presenters = []; this.prefsBranch = Cc['@mozilla.org/preferences-service;1'] .getService(Ci.nsIPrefService).getBranch('accessibility.accessfu.'); this.prefsBranch.addObserver('activate', this, false); - this.prefsBranch.addObserver('explorebytouch', this, false); this.touchAdapter = TouchAdapter; - switch(Utils.MozBuildApp) { + switch (Utils.MozBuildApp) { case 'mobile/android': Services.obs.addObserver(this, 'Accessibility:Settings', false); - Services.obs.addObserver(this, 'Accessibility:NextObject', false); - Services.obs.addObserver(this, 'Accessibility:PreviousObject', false); - Services.obs.addObserver(this, 'Accessibility:CurrentObject', false); + Cc['@mozilla.org/android/bridge;1']. + getService(Ci.nsIAndroidBridge).handleGeckoMessage( + JSON.stringify({ gecko: { type: 'Accessibility:Ready' } })); this.touchAdapter = AndroidTouchAdapter; break; case 'b2g': @@ -67,7 +61,13 @@ var AccessFu = { break; } - this._processPreferences(); + try { + this._activatePref = this.prefsBranch.getIntPref('activate'); + } catch (x) { + this._activatePref = ACCESSFU_DISABLE; + } + + this._enableOrDisable(); }, /** @@ -81,30 +81,24 @@ var AccessFu = { Logger.info('enable'); + for each (let mm in Utils.getAllMessageManagers(this.chromeWin)) + this._loadFrameScript(mm); + // Add stylesheet let stylesheetURL = 'chrome://global/content/accessibility/AccessFu.css'; this.stylesheet = this.chromeWin.document.createProcessingInstruction( 'xml-stylesheet', 'href="' + stylesheetURL + '" type="text/css"'); - this.chromeWin.document.insertBefore(this.stylesheet, this.chromeWin.document.firstChild); + this.chromeWin.document.insertBefore(this.stylesheet, + this.chromeWin.document.firstChild); - this.addPresenter(new VisualPresenter()); + Input.attach(this.chromeWin); + Output.attach(this.chromeWin); + this.touchAdapter.attach(this.chromeWin); - // Implicitly add the Android presenter on Android. - if (Utils.MozBuildApp == 'mobile/android') { - this._androidPresenter = new AndroidPresenter(); - this.addPresenter(this._androidPresenter); - } else if (Utils.MozBuildApp == 'b2g') { - this.addPresenter(new SpeechPresenter()); - } - - 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('focus', this, true); + Services.obs.addObserver(this, 'remote-browser-frame-shown', false); + Services.obs.addObserver(this, 'Accessibility:NextObject', false); + Services.obs.addObserver(this, 'Accessibility:PreviousObject', false); + Services.obs.addObserver(this, 'Accessibility:CurrentObject', false); }, /** @@ -113,380 +107,377 @@ var AccessFu = { _disable: function _disable() { if (!this._enabled) return; + this._enabled = false; Logger.info('disable'); this.chromeWin.document.removeChild(this.stylesheet); + for each (let mm in Utils.getAllMessageManagers(this.chromeWin)) + mm.sendAsyncMessage('AccessFu:Stop'); - this.presenters.forEach(function(p) { p.detach(); }); - this.presenters = []; + Input.detach(); - VirtualCursorController.detach(); - - Services.obs.removeObserver(this, 'accessible-event'); - this.chromeWin.removeEventListener('DOMActivate', this, true); - this.chromeWin.removeEventListener('resize', this, true); - this.chromeWin.removeEventListener('scroll', this, true); - this.chromeWin.removeEventListener('TabOpen', this, true); - this.chromeWin.removeEventListener('focus', this, true); + Services.obs.removeObserver(this, 'remote-browser-frame-shown'); + Services.obs.removeObserver(this, 'Accessibility:NextObject'); + Services.obs.removeObserver(this, 'Accessibility:PreviousObject'); + Services.obs.removeObserver(this, 'Accessibility:CurrentObject'); }, - _processPreferences: function _processPreferences(aEnabled, aTouchEnabled) { - let accessPref = ACCESSFU_DISABLE; + _enableOrDisable: function _enableOrDisable() { try { - accessPref = (aEnabled == undefined) ? - this.prefsBranch.getIntPref('activate') : aEnabled; + if (this._activatePref == ACCESSFU_ENABLE || + this._systemPref && this._activatePref == ACCESSFU_AUTO) + this._enable(); + else + this._disable(); } catch (x) { + Logger.error(x); } - - let ebtPref = ACCESSFU_DISABLE; - try { - ebtPref = (aTouchEnabled == undefined) ? - this.prefsBranch.getIntPref('explorebytouch') : aTouchEnabled; - } catch (x) { - } - - if (Utils.MozBuildApp == 'mobile/android') { - if (accessPref == ACCESSFU_AUTO) { - Cc['@mozilla.org/android/bridge;1']. - getService(Ci.nsIAndroidBridge).handleGeckoMessage( - JSON.stringify({ gecko: { type: 'Accessibility:Ready' } })); - return; - } - } - - if (accessPref == ACCESSFU_ENABLE) - this._enable(); - else - this._disable(); - - if (ebtPref == ACCESSFU_ENABLE) - this.touchAdapter.attach(this.chromeWin); - else - this.touchAdapter.detach(this.chromeWin); }, - addPresenter: function addPresenter(presenter) { - this.presenters.push(presenter); - presenter.attach(this.chromeWin); + receiveMessage: function receiveMessage(aMessage) { + if (Logger.logLevel >= Logger.DEBUG) + Logger.debug('Recieved', aMessage.name, JSON.stringify(aMessage.json)); + + switch (aMessage.name) { + case 'AccessFu:Ready': + let mm = Utils.getMessageManager(aMessage.target); + mm.sendAsyncMessage('AccessFu:Start', + {method: 'start', buildApp: Utils.MozBuildApp}); + break; + case 'AccessFu:Present': + try { + for each (let presenter in aMessage.json) { + Output[presenter.type](presenter.details, aMessage.target); + } + } catch (x) { + Logger.error(x); + } + break; + case 'AccessFu:Input': + Input.setEditState(aMessage.json); + break; + } }, - handleEvent: function handleEvent(aEvent) { - switch (aEvent.type) { - case 'focus': - { - if (aEvent.target instanceof Ci.nsIDOMWindow) { - let docAcc = getAccessible(aEvent.target.document); - let docContext = new PresenterContext(docAcc, null); - let cursorable = docAcc.QueryInterface(Ci.nsIAccessibleCursorable); - let vcContext = new PresenterContext( - (cursorable) ? cursorable.virtualCursor.position : null, null); - this.presenters.forEach( - function(p) { p.tabSelected(docContext, vcContext); }); - } - break; - } - 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; - } - 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; - } - case 'mozContentEvent': - { - if (aEvent.detail.type == 'accessibility-screenreader') { - let pref = aEvent.detail.enabled + 0; - this._processPreferences(pref, pref); - } - break; - } - } + _loadFrameScript: function _loadFrameScript(aMessageManager) { + aMessageManager.addMessageListener('AccessFu:Present', this); + aMessageManager.addMessageListener('AccessFu:Input', this); + aMessageManager.addMessageListener('AccessFu:Ready', this); + aMessageManager. + loadFrameScript( + 'chrome://global/content/accessibility/content-script.js', true); }, observe: function observe(aSubject, aTopic, aData) { + Logger.debug('observe', aTopic); switch (aTopic) { case 'Accessibility:Settings': - this._processPreferences(JSON.parse(aData).enabled + 0, - JSON.parse(aData).exploreByTouch + 0); + this._systemPref = JSON.parse(aData).enabled; + this._enableOrDisable(); break; case 'Accessibility:NextObject': - VirtualCursorController. - moveForward(Utils.getCurrentContentDoc(this.chromeWin)); + Input.moveCursor('moveNext', 'Simple', 'gesture'); break; case 'Accessibility:PreviousObject': - VirtualCursorController. - moveBackward(Utils.getCurrentContentDoc(this.chromeWin)); + Input.moveCursor('movePrevious', 'Simple', 'gesture'); break; case 'Accessibility:CurrentObject': - this._androidPresenter.accessibilityFocus(); + let mm = Utils.getCurrentBrowser(this.chromeWin). + frameLoader.messageManager; + mm.sendAsyncMessage('AccessFu:VirtualCursor', + {action: 'presentLastPivot'}); break; case 'nsPref:changed': - this._processPreferences(this.prefsBranch.getIntPref('activate'), - this.prefsBranch.getIntPref('explorebytouch')); - break; - case 'accessible-event': - let event; - try { - event = aSubject.QueryInterface(Ci.nsIAccessibleEvent); - this._handleAccEvent(event); - } catch (ex) { - Logger.error(ex); - return; + if (aData == 'activate') { + this._activatePref = this.prefsBranch.getIntPref('activate'); + this._enableOrDisable(); } + break; + case 'remote-browser-frame-shown': + { + this._loadFrameScript( + aSubject.QueryInterface(Ci.nsIFrameLoader).messageManager); + break; + } } }, - _handleAccEvent: function _handleAccEvent(aEvent) { - if (Logger.logLevel <= Logger.DEBUG) - Logger.debug(Logger.eventToString(aEvent), - Logger.accessibleToString(aEvent.accessible)); - - switch (aEvent.eventType) { - case Ci.nsIAccessibleEvent.EVENT_VIRTUALCURSOR_CHANGED: - { - let pivot = aEvent.accessible. - QueryInterface(Ci.nsIAccessibleCursorable).virtualCursor; - let event = aEvent. - QueryInterface(Ci.nsIAccessibleVirtualCursorChangeEvent); - let position = pivot.position; - let doc = aEvent.DOMNode; - - let presenterContext = - new PresenterContext(position, event.oldAccessible); - let reason = event.reason; - this.presenters.forEach( - function(p) { p.pivotChanged(presenterContext, reason); }); - - 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'); - } - ); - } - 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'); - } - ); - } - } - break; - } - case Ci.nsIAccessibleEvent.EVENT_REORDER: - { - 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_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 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.presenters.forEach( - function(p) { - p.textChanged(isInserted, event.start, event.length, - text, event.modifiedText); - } - ); - } - break; - } - case Ci.nsIAccessibleEvent.EVENT_SCROLLING_START: - { - VirtualCursorController.moveCursorToObject( - Utils.getVirtualCursor(aEvent.accessibleDocument), aEvent.accessible); - break; - } - case Ci.nsIAccessibleEvent.EVENT_FOCUS: - { - let acc = aEvent.accessible; - let doc = aEvent.accessibleDocument; - if (acc.role != Ci.nsIAccessibleRole.ROLE_DOCUMENT && - doc.role != Ci.nsIAccessibleRole.ROLE_CHROME_WINDOW) - VirtualCursorController.moveCursorToObject( - Utils.getVirtualCursor(doc), acc); - - let [,extState] = Utils.getStates(acc); - let editableState = extState & - (Ci.nsIAccessibleStates.EXT_STATE_EDITABLE | - Ci.nsIAccessibleStates.EXT_STATE_MULTI_LINE); - - if (editableState != VirtualCursorController.editableState) { - if (!VirtualCursorController.editableState) - this.presenters.forEach( - function(p) { - p.editingModeChanged(true); - } - ); - } - VirtualCursorController.editableState = editableState; - break; - } - default: - break; + handleEvent: function handleEvent(aEvent) { + if (aEvent.type == 'mozContentEvent' && + aEvent.detail.type == 'accessibility-screenreader') { + this._systemPref = aEvent.detail.enabled; + this._enableOrDisable(); } }, - /** - * 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:'; - }, - - // A hash of documents that don't yet have an accessible tree. - _pendingDocuments: {}, - // So we don't enable/disable twice _enabled: false }; -function getAccessible(aNode) { - try { - return Cc['@mozilla.org/accessibleRetrieval;1']. - getService(Ci.nsIAccessibleRetrieval).getAccessibleFor(aNode); - } catch (e) { - return null; +var Output = { + attach: function attach(aWindow) { + this.chromeWin = aWindow; + }, + + Speech: function Speech(aDetails, aBrowser) { + for each (let action in aDetails.actions) + Logger.info('tts.' + action.method, '"' + action.data + '"', JSON.stringify(action.options)); + }, + + Visual: function Visual(aDetails, aBrowser) { + if (!this.highlightBox) { + // 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); + } + + if (aDetails.method == 'show') { + let padding = aDetails.padding; + let r = this._adjustBounds(aDetails.bounds, aBrowser); + + // First hide it to avoid flickering when changing the style. + this.highlightBox.style.display = 'none'; + this.highlightBox.style.top = (r.top - padding) + 'px'; + this.highlightBox.style.left = (r.left - padding) + 'px'; + this.highlightBox.style.width = (r.width + padding*2) + 'px'; + this.highlightBox.style.height = (r.height + padding*2) + 'px'; + this.highlightBox.style.display = 'block'; + } else if (aDetails.method == 'hide') { + this.highlightBox.style.display = 'none'; + } + }, + + Android: function Android(aDetails, aBrowser) { + if (!this._bridge) + this._bridge = Cc['@mozilla.org/android/bridge;1'].getService(Ci.nsIAndroidBridge); + + for each (let androidEvent in aDetails) { + androidEvent.type = 'Accessibility:Event'; + if (androidEvent.bounds) + androidEvent.bounds = this._adjustBounds(androidEvent.bounds, aBrowser); + this._bridge.handleGeckoMessage(JSON.stringify({gecko: androidEvent})); + } + }, + + _adjustBounds: function(aJsonBounds, aBrowser) { + let bounds = new Rect(aJsonBounds.left, aJsonBounds.top, + aJsonBounds.right - aJsonBounds.left, + aJsonBounds.bottom - aJsonBounds.top); + let vp = Utils.getViewport(this.chromeWin) || { zoom: 1.0, offsetY: 0 }; + let browserOffset = aBrowser.getBoundingClientRect(); + + return bounds.translate(browserOffset.left, browserOffset.top). + scale(vp.zoom, vp.zoom).expandToIntegers(); } -} +}; + +var Input = { + editState: {}, + + attach: function attach(aWindow) { + this.chromeWin = aWindow; + this.chromeWin.document.addEventListener('keypress', this, true); + this.chromeWin.addEventListener('mozAccessFuGesture', this, true); + }, + + detach: function detach() { + this.chromeWin.document.removeEventListener('keypress', this, true); + this.chromeWin.removeEventListener('mozAccessFuGesture', this, true); + }, + + handleEvent: function Input_handleEvent(aEvent) { + try { + switch (aEvent.type) { + case 'keypress': + this._handleKeypress(aEvent); + break; + case 'mozAccessFuGesture': + this._handleGesture(aEvent); + break; + } + } catch (x) { + Logger.error(x); + } + }, + + _handleGesture: function _handleGesture(aEvent) { + let detail = aEvent.detail; + Logger.info('Gesture', detail.type, + '(fingers: ' + detail.touches.length + ')'); + + if (detail.touches.length == 1) { + switch (detail.type) { + case 'swiperight': + this.moveCursor('moveNext', 'Simple', 'gestures'); + break; + case 'swipeleft': + this.moveCursor('movePrevious', 'Simple', 'gesture'); + break; + case 'doubletap': + this.activateCurrent(); + break; + case 'explore': + this.moveCursor('moveToPoint', 'Simple', 'gesture', + detail.x, detail.y); + break; + } + } + + if (detail.touches.length == 3) { + switch (detail.type) { + case 'swiperight': + this.scroll(-1, true); + break; + case 'swipedown': + this.scroll(-1); + break; + case 'swipeleft': + this.scroll(1, true); + break; + case 'swipeup': + this.scroll(1); + break; + } + } + }, + + _handleKeypress: function _handleKeypress(aEvent) { + let target = aEvent.target; + + // Ignore keys with modifiers so the content could take advantage of them. + if (aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey) + return; + + switch (aEvent.keyCode) { + case 0: + // an alphanumeric key was pressed, handle it separately. + // If it was pressed with either alt or ctrl, just pass through. + // If it was pressed with meta, pass the key on without the meta. + if (this.editState.editing) + return; + + let key = String.fromCharCode(aEvent.charCode); + try { + let [methodName, rule] = this.keyMap[key]; + this.moveCursor(methodName, rule, 'keyboard'); + } catch (x) { + return; + } + break; + case aEvent.DOM_VK_RIGHT: + if (this.editState.editing) { + if (!this.editState.atEnd) + // Don't move forward if caret is not at end of entry. + // XXX: Fix for rtl + return; + else + target.blur(); + } + this.moveCursor(aEvent.shiftKey ? 'moveLast' : 'moveNext', 'Simple', 'keyboard'); + break; + case aEvent.DOM_VK_LEFT: + if (this.editState.editing) { + if (!this.editState.atStart) + // Don't move backward if caret is not at start of entry. + // XXX: Fix for rtl + return; + else + target.blur(); + } + this.moveCursor(aEvent.shiftKey ? 'moveFirst' : 'movePrevious', 'Simple', 'keyboard'); + break; + case aEvent.DOM_VK_UP: + if (this.editState.multiline) { + if (!this.editState.atStart) + // Don't blur content if caret is not at start of text area. + return; + else + target.blur(); + } + + if (Utils.MozBuildApp == 'mobile/android') + // Return focus to native Android browser chrome. + Cc['@mozilla.org/android/bridge;1']. + getService(Ci.nsIAndroidBridge).handleGeckoMessage( + JSON.stringify({ gecko: { type: 'ToggleChrome:Focus' } })); + break; + case aEvent.DOM_VK_RETURN: + case aEvent.DOM_VK_ENTER: + if (this.editState.editing) + return; + this.activateCurrent(); + break; + default: + return; + } + + aEvent.preventDefault(); + aEvent.stopPropagation(); + }, + + moveCursor: function moveCursor(aAction, aRule, aInputType, aX, aY) { + let mm = Utils.getMessageManager(Utils.getCurrentBrowser(this.chromeWin)); + mm.sendAsyncMessage('AccessFu:VirtualCursor', + {action: aAction, rule: aRule, + x: aX, y: aY, origin: 'top', + inputType: aInputType}); + }, + + activateCurrent: function activateCurrent() { + let mm = Utils.getMessageManager(Utils.getCurrentBrowser(this.chromeWin)); + mm.sendAsyncMessage('AccessFu:Activate', {}); + }, + + setEditState: function setEditState(aEditState) { + this.editState = aEditState; + }, + + scroll: function scroll(aPage, aHorizontal) { + let mm = Utils.getMessageManager(Utils.getCurrentBrowser(this.chromeWin)); + mm.sendAsyncMessage('AccessFu:Scroll', {page: aPage, horizontal: aHorizontal, origin: 'top'}); + }, + + keyMap: { + a: ['moveNext', 'Anchor'], + A: ['movePrevious', 'Anchor'], + b: ['moveNext', 'Button'], + B: ['movePrevious', 'Button'], + c: ['moveNext', 'Combobox'], + C: ['movePrevious', 'Combobox'], + e: ['moveNext', 'Entry'], + E: ['movePrevious', 'Entry'], + f: ['moveNext', 'FormElement'], + F: ['movePrevious', 'FormElement'], + g: ['moveNext', 'Graphic'], + G: ['movePrevious', 'Graphic'], + h: ['moveNext', 'Heading'], + H: ['movePrevious', 'Heading'], + i: ['moveNext', 'ListItem'], + I: ['movePrevious', 'ListItem'], + k: ['moveNext', 'Link'], + K: ['movePrevious', 'Link'], + l: ['moveNext', 'List'], + L: ['movePrevious', 'List'], + p: ['moveNext', 'PageTab'], + P: ['movePrevious', 'PageTab'], + r: ['moveNext', 'RadioButton'], + R: ['movePrevious', 'RadioButton'], + s: ['moveNext', 'Separator'], + S: ['movePrevious', 'Separator'], + t: ['moveNext', 'Table'], + T: ['movePrevious', 'Table'], + x: ['moveNext', 'Checkbox'], + X: ['movePrevious', 'Checkbox'] + } +}; diff --git a/accessible/src/jsat/EventManager.jsm b/accessible/src/jsat/EventManager.jsm new file mode 100644 index 000000000000..913ecab7a6d3 --- /dev/null +++ b/accessible/src/jsat/EventManager.jsm @@ -0,0 +1,299 @@ +/* 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/. */ + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; + +Cu.import('resource://gre/modules/accessibility/Utils.jsm'); +Cu.import('resource://gre/modules/accessibility/Presenters.jsm'); +Cu.import('resource://gre/modules/accessibility/TraversalRules.jsm'); +Cu.import('resource://gre/modules/Services.jsm'); +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); + +var EXPORTED_SYMBOLS = ['EventManager']; + +var EventManager = { + editState: {}, + + start: function start(aSendMsgFunc) { + try { + if (!this._started) { + this.sendMsgFunc = aSendMsgFunc || function() {}; + this.presenters = [new VisualPresenter()]; + + if (Utils.MozBuildApp == 'b2g') { + this.presenters.push(new SpeechPresenter()); + } else if (Utils.MozBuildApp == 'mobile/android') { + this.presenters.push(new AndroidPresenter()); + } + + Logger.info('EventManager.start', Utils.MozBuildApp, [p.type for each(p in this.presenters)].join(', ')); + + this._started = true; + Services.obs.addObserver(this, 'accessible-event', false); + } + + this.present( + function(p) { + return p.tabStateChanged(null, 'newtab'); + } + ); + } catch (x) { + Logger.error('Failed to start EventManager:', x); + } + }, + + stop: function stop() { + Services.obs.removeObserver(this, 'accessible-event'); + this.presenters = []; + this._started = false; + }, + + handleEvent: function handleEvent(aEvent) { + try { + switch (aEvent.type) { + case 'DOMActivate': + { + let activatedAcc = + Utils.AccRetrieval.getAccessibleFor(aEvent.originalTarget); + let [state, extState] = Utils.getStates(activatedAcc); + + // 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 & Ci.nsIAccessibleStates.STATE_CHECKABLE) + return; + + this.present( + function(p) { + return p.actionInvoked(activatedAcc, 'click'); + } + ); + break; + } + case 'scroll': + case 'resize': + { + this.present( + function(p) { + return p.viewportChanged();; + } + ); + break; + } + } + } catch (x) { + Logger.error('Error handling DOM event:', x); + } + }, + + observe: function observe(aSubject, aTopic, aData) { + switch (aTopic) { + case 'accessible-event': + var event; + try { + event = aSubject.QueryInterface(Ci.nsIAccessibleEvent); + this.handleAccEvent(event); + } catch (x) { + Logger.error('Error handing accessible event:', x); + return; + } + } + }, + + presentLastPivot: function presentLastPivot() { + this.present( + function(p) { + return p.presentLastPivot(); + } + ); + }, + + handleAccEvent: function handleAccEvent(aEvent) { + if (Logger.logLevel >= Logger.DEBUG) + Logger.debug('A11yEvent', Logger.eventToString(aEvent), + Logger.accessibleToString(aEvent.accessible)); + + switch (aEvent.eventType) { + case Ci.nsIAccessibleEvent.EVENT_VIRTUALCURSOR_CHANGED: + { + let pivot = aEvent.accessible. + QueryInterface(Ci.nsIAccessibleCursorable).virtualCursor; + let position = pivot.position; + if (position.role == Ci.nsIAccessibleRole.ROLE_INTERNAL_FRAME) + break; + let event = aEvent. + QueryInterface(Ci.nsIAccessibleVirtualCursorChangeEvent); + let presenterContext = + new PresenterContext(position, event.oldAccessible); + let reason = event.reason; + + if (this.editState.editing) + aEvent.accessibleDocument.takeFocus(); + + this.present( + function(p) { + return p.pivotChanged(presenterContext, reason); + } + ); + break; + } + case Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE: + { + let event = aEvent.QueryInterface(Ci.nsIAccessibleStateChangeEvent); + if (event.state == Ci.nsIAccessibleStates.STATE_CHECKED && + !(event.isExtraState())) { + this.present( + function(p) { + return p.actionInvoked(aEvent.accessible, + event.isEnabled() ? 'check' : 'uncheck'); + } + ); + } + break; + } + case Ci.nsIAccessibleEvent.EVENT_SCROLLING_START: + { + let vc = Utils.getVirtualCursor(aEvent.accessibleDocument); + vc.moveNext(TraversalRules.Simple, aEvent.accessible, true); + break; + } + case Ci.nsIAccessibleEvent.EVENT_TEXT_CARET_MOVED: + { + let acc = aEvent.accessible; + let characterCount = acc. + QueryInterface(Ci.nsIAccessibleText).characterCount; + let caretOffset = aEvent. + QueryInterface(Ci.nsIAccessibleCaretMoveEvent).caretOffset; + + // Update editing state, both for presenter and other things + let [,extState] = Utils.getStates(acc); + let editState = { + editing: !!(extState & Ci.nsIAccessibleStates.EXT_STATE_EDITABLE), + multiline: !!(extState & Ci.nsIAccessibleStates.EXT_STATE_MULTI_LINE), + atStart: caretOffset == 0, + atEnd: caretOffset == characterCount + }; + + // Not interesting + if (!editState.editing && editState.editing == this.editState.editing) + break; + + if (editState.editing != this.editState.editing) + this.present( + function(p) { + return p.editingModeChanged(editState.editing); + } + ); + + if (editState.editing != this.editState.editing || + editState.multiline != this.editState.multiline || + editState.atEnd != this.editState.atEnd || + editState.atStart != this.editState.atStart) + this.sendMsgFunc("AccessFu:Input", editState); + + this.editState = editState; + break; + } + 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 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( + function(p) { + return p.textChanged(isInserted, event.start, event.length, + text, event.modifiedText); + } + ); + } + break; + } + case Ci.nsIAccessibleEvent.EVENT_FOCUS: + { + // Put vc where the focus is at + let acc = aEvent.accessible; + let doc = aEvent.accessibleDocument; + if (acc.role != Ci.nsIAccessibleRole.ROLE_DOCUMENT && + doc.role != Ci.nsIAccessibleRole.ROLE_CHROME_WINDOW) { + let vc = Utils.getVirtualCursor(doc); + vc.moveNext(TraversalRules.Simple, acc, true); + } + break; + } + } + }, + + present: function present(aPresenterFunc) { + try { + this.sendMsgFunc( + "AccessFu:Present", + [aPresenterFunc(p) for each (p in this.presenters)]. + filter(function(d) {return !!d;})); + } catch (x) { + Logger.error(x); + } + }, + + onStateChange: function onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + let tabstate = ''; + + let loadingState = Ci.nsIWebProgressListener.STATE_TRANSFERRING | + Ci.nsIWebProgressListener.STATE_IS_DOCUMENT; + let loadedState = Ci.nsIWebProgressListener.STATE_STOP | + Ci.nsIWebProgressListener.STATE_IS_NETWORK; + + if ((aStateFlags & loadingState) == loadingState) { + tabstate = 'loading'; + } else if ((aStateFlags & loadedState) == loadedState && + !aWebProgress.isLoadingDocument) { + tabstate = 'loaded'; + } + + if (tabstate) { + let docAcc = Utils.AccRetrieval.getAccessibleFor(aWebProgress.DOMWindow.document); + this.present( + function(p) { + return p.tabStateChanged(docAcc, tabstate); + } + ); + } + }, + + onProgressChange: function onProgressChange() {}, + + onLocationChange: function onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { + let docAcc = Utils.AccRetrieval.getAccessibleFor(aWebProgress.DOMWindow.document); + this.present( + function(p) { + return p.tabStateChanged(docAcc, 'newdoc'); + } + ); + }, + + onStatusChange: function onStatusChange() {}, + + onSecurityChange: function onSecurityChange() {}, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference, + Ci.nsISupports, + Ci.nsIObserver]) +}; diff --git a/accessible/src/jsat/Presenters.jsm b/accessible/src/jsat/Presenters.jsm index a2fed62649b5..b037ce057706 100644 --- a/accessible/src/jsat/Presenters.jsm +++ b/accessible/src/jsat/Presenters.jsm @@ -26,6 +26,11 @@ var EXPORTED_SYMBOLS = ['VisualPresenter', function Presenter() {} Presenter.prototype = { + /** + * The type of presenter. Used for matching it with the appropriate output method. + */ + type: 'Base', + /** * Attach function for presenter. * @param {ChromeWindow} aWindow Chrome window the presenter could use. @@ -92,13 +97,19 @@ Presenter.prototype = { /** * The viewport has changed, either a scroll, pan, zoom, or * landscape/portrait toggle. + * @param {Window} aWindow window of viewport that changed. */ - viewportChanged: function viewportChanged() {}, + viewportChanged: function viewportChanged(aWindow) {}, /** * We have entered or left text editing mode. */ - editingModeChanged: function editingModeChanged(aIsEditing) {} + editingModeChanged: function editingModeChanged(aIsEditing) {}, + + /** + * Re-present the last pivot change. + */ + presentLastPivot: function AndroidPresenter_presentLastPivot() {} }; /** @@ -110,83 +121,60 @@ function VisualPresenter() {} VisualPresenter.prototype = { __proto__: Presenter.prototype, + type: 'Visual', + /** * The padding in pixels between the object and the highlight border. */ BORDER_PADDING: 2, - attach: function VisualPresenter_attach(aWindow) { - this.chromeWin = aWindow; - - // 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); - }, - - detach: function VisualPresenter_detach() { - this.highlightBox.parentNode.removeChild(this.highlightBox); - this.highlightBox = this.stylesheet = null; - }, - - viewportChanged: function VisualPresenter_viewportChanged() { + viewportChanged: function VisualPresenter_viewportChanged(aWindow) { if (this._currentContext) - this._highlight(this._currentContext); + return { + type: this.type, + details: { + method: 'show', + bounds: this._currentContext.bounds, + padding: this.BORDER_PADDING + } + }; + + return null; }, pivotChanged: function VisualPresenter_pivotChanged(aContext, aReason) { this._currentContext = aContext; - if (!aContext.accessible) { - this._hide(); - return; - } + if (!aContext.accessible) + return {type: this.type, details: {method: 'hide'}}; try { aContext.accessible.scrollTo( Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE); - this._highlight(aContext); + return { + type: this.type, + details: { + method: 'show', + bounds: aContext.bounds, + padding: this.BORDER_PADDING + } + }; } catch (e) { Logger.error('Failed to get bounds: ' + e); - return; + return null; } }, tabSelected: function VisualPresenter_tabSelected(aDocContext, aVCContext) { - this.pivotChanged(aVCContext, Ci.nsIAccessiblePivot.REASON_NONE); + return this.pivotChanged(aVCContext, Ci.nsIAccessiblePivot.REASON_NONE); }, tabStateChanged: function VisualPresenter_tabStateChanged(aDocObj, aPageState) { if (aPageState == 'newdoc') - this._hide(); - }, + return {type: this.type, details: {method: 'hide'}}; - // Internals - - _hide: function _hide() { - this.highlightBox.style.display = 'none'; - }, - - _highlight: function _highlight(aContext) { - let vp = Utils.getViewport(this.chromeWin) || { zoom: 1.0, offsetY: 0 }; - let r = aContext.bounds.scale(vp.zoom, vp.zoom).expandToIntegers(); - - // First hide it to avoid flickering when changing the style. - this.highlightBox.style.display = 'none'; - this.highlightBox.style.top = (r.top - this.BORDER_PADDING) + 'px'; - this.highlightBox.style.left = (r.left - this.BORDER_PADDING) + 'px'; - this.highlightBox.style.width = (r.width + this.BORDER_PADDING*2) + 'px'; - this.highlightBox.style.height = (r.height + this.BORDER_PADDING*2) + 'px'; - this.highlightBox.style.display = 'block'; + return null; } }; @@ -199,6 +187,8 @@ function AndroidPresenter() {} AndroidPresenter.prototype = { __proto__: Presenter.prototype, + type: 'Android', + // Android AccessibilityEvent type constants. ANDROID_VIEW_CLICKED: 0x01, ANDROID_VIEW_LONG_CLICKED: 0x02, @@ -212,16 +202,14 @@ AndroidPresenter.prototype = { ANDROID_ANNOUNCEMENT: 0x4000, ANDROID_VIEW_ACCESSIBILITY_FOCUSED: 0x8000, - attach: function AndroidPresenter_attach(aWindow) { - this.chromeWin = aWindow; - }, - pivotChanged: function AndroidPresenter_pivotChanged(aContext, aReason) { if (!aContext.accessible) - return; + return null; this._currentContext = aContext; + let androidEvents = []; + let isExploreByTouch = (aReason == Ci.nsIAccessiblePivot.REASON_POINT && Utils.AndroidSdkVersion >= 14); let focusEventType = (Utils.AndroidSdkVersion >= 16) ? @@ -231,17 +219,9 @@ AndroidPresenter.prototype = { if (isExploreByTouch) { // This isn't really used by TalkBack so this is a half-hearted attempt // for now. - this.sendMessageToJava({ - gecko: { - type: 'Accessibility:Event', - eventType: this.ANDROID_VIEW_HOVER_EXIT, - text: [] - } - }); + androidEvents.push({eventType: this.ANDROID_VIEW_HOVER_EXIT, text: []}); } - let vp = Utils.getViewport(this.chromeWin) || { zoom: 1.0, offsetY: 0 }; - let bounds = aContext.bounds.scale(vp.zoom, vp.zoom).expandToIntegers(); let output = []; aContext.newAncestry.forEach( @@ -259,34 +239,34 @@ AndroidPresenter.prototype = { } ); - this.sendMessageToJava({ - gecko: { - type: 'Accessibility:Event', - eventType: (isExploreByTouch) ? this.ANDROID_VIEW_HOVER_ENTER : focusEventType, - text: output, - bounds: bounds - } - }); + androidEvents.push({eventType: (isExploreByTouch) ? + this.ANDROID_VIEW_HOVER_ENTER : focusEventType, + text: output, + bounds: aContext.bounds}); + return { + type: this.type, + details: androidEvents + }; }, actionInvoked: function AndroidPresenter_actionInvoked(aObject, aActionName) { - this.sendMessageToJava({ - gecko: { - type: 'Accessibility:Event', + return { + type: this.type, + details: [{ eventType: this.ANDROID_VIEW_CLICKED, text: UtteranceGenerator.genForAction(aObject, aActionName) - } - }); + }] + }; }, tabSelected: function AndroidPresenter_tabSelected(aDocContext, aVCContext) { // Send a pivot change message with the full context utterance for this doc. - this.pivotChanged(aVCContext, Ci.nsIAccessiblePivot.REASON_NONE); + return this.pivotChanged(aVCContext, Ci.nsIAccessiblePivot.REASON_NONE); }, tabStateChanged: function AndroidPresenter_tabStateChanged(aDocObj, aPageState) { - this._appAnnounce( + return this._appAnnounce( UtteranceGenerator.genForTabStateChange(aDocObj, aPageState)); }, @@ -294,12 +274,14 @@ AndroidPresenter.prototype = { aLength, aText, aModifiedText) { let androidEvent = { - type: 'Accessibility:Event', - eventType: this.ANDROID_VIEW_TEXT_CHANGED, - text: [aText], - fromIndex: aStart, - removedCount: 0, - addedCount: 0 + type: this.type, + details: [{ + eventType: this.ANDROID_VIEW_TEXT_CHANGED, + text: [aText], + fromIndex: aStart, + removedCount: 0, + addedCount: 0 + }] }; if (aIsInserted) { @@ -312,71 +294,52 @@ AndroidPresenter.prototype = { aText.substring(0, aStart) + aModifiedText + aText.substring(aStart); } - this.sendMessageToJava({gecko: androidEvent}); + return androidEvent; }, - viewportChanged: function AndroidPresenter_viewportChanged() { + viewportChanged: function AndroidPresenter_viewportChanged(aWindow) { if (Utils.AndroidSdkVersion < 14) - return; + return null; - let win = Utils.getBrowserApp(this.chromeWin).selectedBrowser.contentWindow; - this.sendMessageToJava({ - gecko: { - type: 'Accessibility:Event', + return { + type: this.type, + details: [{ eventType: this.ANDROID_VIEW_SCROLLED, text: [], - scrollX: win.scrollX, - scrollY: win.scrollY, - maxScrollX: win.scrollMaxX, - maxScrollY: win.scrollMaxY - } - }); + scrollX: aWindow.scrollX, + scrollY: aWindow.scrollY, + maxScrollX: aWindow.scrollMaxX, + maxScrollY: aWindow.scrollMaxY + }] + }; }, editingModeChanged: function AndroidPresenter_editingModeChanged(aIsEditing) { - this._appAnnounce(UtteranceGenerator.genForEditingMode(aIsEditing)); + return this._appAnnounce(UtteranceGenerator.genForEditingMode(aIsEditing)); }, _appAnnounce: function _appAnnounce(aUtterance) { if (!aUtterance.length) - return; + return null; - this.sendMessageToJava({ - gecko: { - type: 'Accessibility:Event', + return { + type: this.type, + details: [{ eventType: (Utils.AndroidSdkVersion >= 16) ? this.ANDROID_ANNOUNCEMENT : this.ANDROID_VIEW_TEXT_CHANGED, text: aUtterance, addedCount: aUtterance.join(' ').length, removedCount: 0, fromIndex: 0 - } - }); + }] + }; }, - accessibilityFocus: function AndroidPresenter_accessibilityFocus() { + presentLastPivot: function AndroidPresenter_presentLastPivot() { if (this._currentContext) - this.pivotChanged(this._currentContext); - }, - - sendMessageToJava: function AndroidPresenter_sendMessageTojava(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 = { - __proto__: AndroidPresenter.prototype, - - sendMessageToJava: function DummyAndroidPresenter_sendMessageToJava(aMsg) { - Logger.debug('Android event:\n' + JSON.stringify(aMsg, null, 2)); + return this.pivotChanged(this._currentContext); + else + return null; } }; @@ -389,10 +352,11 @@ function SpeechPresenter() {} SpeechPresenter.prototype = { __proto__: Presenter.prototype, + type: 'Speech', pivotChanged: function SpeechPresenter_pivotChanged(aContext, aReason) { if (!aContext.accessible) - return; + return null; let output = []; @@ -411,9 +375,17 @@ SpeechPresenter.prototype = { } ); - Logger.info('SPEAK', '"' + output.join(' ') + '"'); + return { + type: this.type, + details: { + actions: [ + {method: 'playEarcon', data: 'tick', options: {}}, + {method: 'speak', data: output.join(' '), options: {enqueue: true}} + ] + } + }; } -} +}; /** * PresenterContext: An object that generates and caches context information @@ -504,7 +476,8 @@ PresenterContext.prototype = { this._accessible.getBounds(objX, objY, objW, objH); - // Can't specify relative coords in nsIAccessible.getBounds, so we do it. + // XXX: OOP content provides a screen offset of 0, while in-process provides a real + // offset. Removing the offset and using content-relative coords normalizes this. let docX = {}, docY = {}; let docRoot = this._accessible.rootDocument. QueryInterface(Ci.nsIAccessible); diff --git a/accessible/src/jsat/TraversalRules.jsm b/accessible/src/jsat/TraversalRules.jsm new file mode 100644 index 000000000000..0d11c8785478 --- /dev/null +++ b/accessible/src/jsat/TraversalRules.jsm @@ -0,0 +1,207 @@ +/* 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 = ['TraversalRules']; + +Cu.import('resource://gre/modules/accessibility/Utils.jsm'); +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); + +function BaseTraversalRule(aRoles, aMatchFunc) { + this._matchRoles = aRoles; + this._matchFunc = aMatchFunc; +} + +BaseTraversalRule.prototype = { + getMatchRoles: function BaseTraversalRule_getmatchRoles(aRules) { + aRules.value = this._matchRoles; + return aRules.value.length; + }, + + preFilter: Ci.nsIAccessibleTraversalRule.PREFILTER_DEFUNCT | + Ci.nsIAccessibleTraversalRule.PREFILTER_INVISIBLE, + + match: function BaseTraversalRule_match(aAccessible) + { + if (aAccessible.role == Ci.nsIAccessibleRole.ROLE_INTERNAL_FRAME) { + return (aAccessible.childCount) ? + Ci.nsIAccessibleTraversalRule.FILTER_IGNORE : + Ci.nsIAccessibleTraversalRule.FILTER_MATCH; + } + + if (this._matchFunc) + return this._matchFunc(aAccessible); + + return Ci.nsIAccessibleTraversalRule.FILTER_MATCH; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAccessibleTraversalRule]) +}; + +var gSimpleTraversalRoles = + [Ci.nsIAccessibleRole.ROLE_MENUITEM, + Ci.nsIAccessibleRole.ROLE_LINK, + Ci.nsIAccessibleRole.ROLE_PAGETAB, + Ci.nsIAccessibleRole.ROLE_GRAPHIC, + // XXX: Find a better solution for ROLE_STATICTEXT. + // It allows to filter list bullets but at the same time it + // filters CSS generated content too as an unwanted side effect. + // Ci.nsIAccessibleRole.ROLE_STATICTEXT, + Ci.nsIAccessibleRole.ROLE_TEXT_LEAF, + Ci.nsIAccessibleRole.ROLE_PUSHBUTTON, + Ci.nsIAccessibleRole.ROLE_CHECKBUTTON, + Ci.nsIAccessibleRole.ROLE_RADIOBUTTON, + Ci.nsIAccessibleRole.ROLE_COMBOBOX, + Ci.nsIAccessibleRole.ROLE_PROGRESSBAR, + Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWN, + Ci.nsIAccessibleRole.ROLE_BUTTONMENU, + Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM, + Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT, + Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM, + Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON, + Ci.nsIAccessibleRole.ROLE_ENTRY, + // Used for traversing in to child OOP frames. + Ci.nsIAccessibleRole.ROLE_INTERNAL_FRAME]; + +var TraversalRules = { + Simple: new BaseTraversalRule( + gSimpleTraversalRoles, + function Simple_match(aAccessible) { + switch (aAccessible.role) { + case Ci.nsIAccessibleRole.ROLE_COMBOBOX: + // We don't want to ignore the subtree because this is often + // where the list box hangs out. + return Ci.nsIAccessibleTraversalRule.FILTER_MATCH; + case Ci.nsIAccessibleRole.ROLE_TEXT_LEAF: + { + // Nameless text leaves are boring, skip them. + let name = aAccessible.name; + if (name && name.trim()) + return Ci.nsIAccessibleTraversalRule.FILTER_MATCH; + else + return Ci.nsIAccessibleTraversalRule.FILTER_IGNORE; + } + case Ci.nsIAccessibleRole.ROLE_LINK: + // If the link has children we should land on them instead. + // Image map links don't have children so we need to match those. + if (aAccessible.childCount == 0) + return Ci.nsIAccessibleTraversalRule.FILTER_MATCH; + else + return Ci.nsIAccessibleTraversalRule.FILTER_IGNORE; + default: + // Ignore the subtree, if there is one. So that we don't land on + // the same content that was already presented by its parent. + return Ci.nsIAccessibleTraversalRule.FILTER_MATCH | + Ci.nsIAccessibleTraversalRule.FILTER_IGNORE_SUBTREE; + } + } + ), + + SimpleTouch: new BaseTraversalRule( + gSimpleTraversalRoles, + function Simple_match(aAccessible) { + return Ci.nsIAccessibleTraversalRule.FILTER_MATCH | + Ci.nsIAccessibleTraversalRule.FILTER_IGNORE_SUBTREE; + } + ), + + Anchor: new BaseTraversalRule( + [Ci.nsIAccessibleRole.ROLE_LINK], + function Anchor_match(aAccessible) + { + // We want to ignore links, only focus named anchors. + let state = {}; + let extraState = {}; + aAccessible.getState(state, extraState); + if (state.value & Ci.nsIAccessibleStates.STATE_LINKED) { + return Ci.nsIAccessibleTraversalRule.FILTER_IGNORE; + } else { + return Ci.nsIAccessibleTraversalRule.FILTER_MATCH; + } + }), + + Button: new BaseTraversalRule( + [Ci.nsIAccessibleRole.ROLE_PUSHBUTTON, + Ci.nsIAccessibleRole.ROLE_SPINBUTTON, + Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON, + Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWN, + Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWNGRID]), + + Combobox: new BaseTraversalRule( + [Ci.nsIAccessibleRole.ROLE_COMBOBOX, + Ci.nsIAccessibleRole.ROLE_LISTBOX]), + + Entry: new BaseTraversalRule( + [Ci.nsIAccessibleRole.ROLE_ENTRY, + Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT]), + + FormElement: new BaseTraversalRule( + [Ci.nsIAccessibleRole.ROLE_PUSHBUTTON, + Ci.nsIAccessibleRole.ROLE_SPINBUTTON, + Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON, + Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWN, + Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWNGRID, + Ci.nsIAccessibleRole.ROLE_COMBOBOX, + Ci.nsIAccessibleRole.ROLE_LISTBOX, + Ci.nsIAccessibleRole.ROLE_ENTRY, + Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT, + Ci.nsIAccessibleRole.ROLE_PAGETAB, + Ci.nsIAccessibleRole.ROLE_RADIOBUTTON, + Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM, + Ci.nsIAccessibleRole.ROLE_SLIDER, + Ci.nsIAccessibleRole.ROLE_CHECKBUTTON, + Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM]), + + Graphic: new BaseTraversalRule( + [Ci.nsIAccessibleRole.ROLE_GRAPHIC]), + + Heading: new BaseTraversalRule( + [Ci.nsIAccessibleRole.ROLE_HEADING]), + + ListItem: new BaseTraversalRule( + [Ci.nsIAccessibleRole.ROLE_LISTITEM, + Ci.nsIAccessibleRole.ROLE_TERM]), + + Link: new BaseTraversalRule( + [Ci.nsIAccessibleRole.ROLE_LINK], + function Link_match(aAccessible) + { + // We want to ignore anchors, only focus real links. + let state = {}; + let extraState = {}; + aAccessible.getState(state, extraState); + if (state.value & Ci.nsIAccessibleStates.STATE_LINKED) { + return Ci.nsIAccessibleTraversalRule.FILTER_MATCH; + } else { + return Ci.nsIAccessibleTraversalRule.FILTER_IGNORE; + } + }), + + List: new BaseTraversalRule( + [Ci.nsIAccessibleRole.ROLE_LIST, + Ci.nsIAccessibleRole.ROLE_DEFINITION_LIST]), + + PageTab: new BaseTraversalRule( + [Ci.nsIAccessibleRole.ROLE_PAGETAB]), + + RadioButton: new BaseTraversalRule( + [Ci.nsIAccessibleRole.ROLE_RADIOBUTTON, + Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM]), + + Separator: new BaseTraversalRule( + [Ci.nsIAccessibleRole.ROLE_SEPARATOR]), + + Table: new BaseTraversalRule( + [Ci.nsIAccessibleRole.ROLE_TABLE]), + + Checkbox: new BaseTraversalRule( + [Ci.nsIAccessibleRole.ROLE_CHECKBUTTON, + Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM]) +}; diff --git a/accessible/src/jsat/Utils.jsm b/accessible/src/jsat/Utils.jsm index 02c3c5924023..41fc26b5bb26 100644 --- a/accessible/src/jsat/Utils.jsm +++ b/accessible/src/jsat/Utils.jsm @@ -29,6 +29,10 @@ var Utils = { return this._AccRetrieval; }, + set MozBuildApp(value) { + this._buildApp = value; + }, + get MozBuildApp() { if (!this._buildApp) this._buildApp = this._buildAppMap[Services.appinfo.ID]; @@ -41,6 +45,13 @@ var Utils = { return this._OS; }, + get ScriptName() { + if (!this._ScriptName) + this._ScriptName = + (Services.appinfo.processType == 2) ? 'AccessFuContent' : 'AccessFu'; + return this._ScriptName; + }, + get AndroidSdkVersion() { if (!this._AndroidSdkVersion) { let shellVersion = Services.sysinfo.get('shellVersion') || ''; @@ -71,24 +82,41 @@ var Utils = { } }, - getCurrentContentDoc: function getCurrentContentDoc(aWindow) { - if (this.MozBuildApp == "b2g") - return this.getBrowserApp(aWindow).contentBrowser.contentDocument; - return this.getBrowserApp(aWindow).selectedBrowser.contentDocument; + getCurrentBrowser: function getCurrentBrowser(aWindow) { + if (this.MozBuildApp == 'b2g') + return this.getBrowserApp(aWindow).contentBrowser; + return this.getBrowserApp(aWindow).selectedBrowser; }, - getAllDocuments: function getAllDocuments(aWindow) { - let doc = this.AccRetrieval. - getAccessibleFor(this.getCurrentContentDoc(aWindow)). - QueryInterface(Ci.nsIAccessibleDocument); - let docs = []; - function getAllDocuments(aDocument) { - docs.push(aDocument.DOMDocument); - for (let i = 0; i < aDocument.childDocumentCount; i++) - getAllDocuments(aDocument.getChildDocumentAt(i)); + getCurrentContentDoc: function getCurrentContentDoc(aWindow) { + return this.getCurrentBrowser(aWindow).contentDocument; + }, + + getMessageManager: function getMessageManager(aBrowser) { + try { + return aBrowser.QueryInterface(Ci.nsIFrameLoaderOwner). + frameLoader.messageManager; + } catch (x) { + Logger.error(x); + return null; } - getAllDocuments(doc); - return docs; + }, + + getAllMessageManagers: function getAllMessageManagers(aWindow) { + let messageManagers = []; + + for (let i = 0; i < aWindow.messageManager.childCount; i++) + messageManagers.push(aWindow.messageManager.getChildAt(i)); + + let remoteframes = this.getCurrentContentDoc(aWindow). + querySelectorAll('iframe[remote=true]'); + + for (let i = 0; i < remoteframes.length; ++i) + messageManagers.push(this.getMessageManager(remoteframes[i])); + + Logger.info(messageManagers.length); + + return messageManagers; }, getViewport: function getViewport(aWindow) { @@ -123,83 +151,6 @@ var Utils = { } return null; - }, - - scroll: function scroll(aWindow, aPage, aHorizontal) { - for each (let doc in this.getAllDocuments(aWindow)) { - // First see if we could scroll a window. - let win = doc.defaultView; - if (!aHorizontal && win.scrollMaxY && - ((aPage > 0 && win.scrollY < win.scrollMaxY) || - (aPage < 0 && win.scrollY > 0))) { - win.scroll(0, win.innerHeight); - return true; - } else if (aHorizontal && win.scrollMaxX && - ((aPage > 0 && win.scrollX < win.scrollMaxX) || - (aPage < 0 && win.scrollX > 0))) { - win.scroll(win.innerWidth, 0); - return true; - } - - // Second, try to scroll main section or current target if there is no - // main section. - let main = doc.querySelector('[role=main]') || - doc.querySelector(':target'); - - if (main) { - if ((!aHorizontal && main.clientHeight < main.scrollHeight) || - (aHorizontal && main.clientWidth < main.scrollWidth)) { - let s = win.getComputedStyle(main); - if (!aHorizontal) { - if (s.overflowY == 'scroll' || s.overflowY == 'auto') { - main.scrollTop += aPage * main.clientHeight; - return true; - } - } else { - if (s.overflowX == 'scroll' || s.overflowX == 'auto') { - main.scrollLeft += aPage * main.clientWidth; - return true; - } - } - } - } - } - - return false; - }, - - changePage: function changePage(aWindow, aPage) { - for each (let doc in this.getAllDocuments(aWindow)) { - // Get current main section or active target. - let main = doc.querySelector('[role=main]') || - doc.querySelector(':target'); - if (!main) - continue; - - let mainAcc = this.AccRetrieval.getAccessibleFor(main); - if (!mainAcc) - continue; - - let controllers = mainAcc. - getRelationByType(Ci.nsIAccessibleRelation.RELATION_CONTROLLED_BY); - - for (var i=0; controllers.targetsCount > i; i++) { - let controller = controllers.getTarget(i); - // If the section has a controlling slider, it should be considered - // the page-turner. - if (controller.role == Ci.nsIAccessibleRole.ROLE_SLIDER) { - // Sliders are controlled with ctrl+right/left. I just decided :) - let evt = doc.createEvent("KeyboardEvent"); - evt.initKeyEvent('keypress', true, true, null, - true, false, false, false, - (aPage > 0) ? evt.DOM_VK_RIGHT : evt.DOM_VK_LEFT, 0); - controller.DOMNode.dispatchEvent(evt); - return true; - } - } - } - - return false; } }; @@ -217,7 +168,8 @@ var Logger = { return; let message = Array.prototype.slice.call(arguments, 1).join(' '); - dump('[AccessFu] ' + this._LEVEL_NAMES[aLogLevel] + ' ' + message + '\n'); + dump('[' + Utils.ScriptName + '] ' + + this._LEVEL_NAMES[aLogLevel] +' ' + message + '\n'); }, info: function info() { diff --git a/accessible/src/jsat/VirtualCursorController.jsm b/accessible/src/jsat/VirtualCursorController.jsm deleted file mode 100644 index 16532d3be50d..000000000000 --- a/accessible/src/jsat/VirtualCursorController.jsm +++ /dev/null @@ -1,434 +0,0 @@ -/* 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/accessibility/Utils.jsm'); -Cu.import('resource://gre/modules/XPCOMUtils.jsm'); - -function BaseTraversalRule(aRoles, aMatchFunc) { - this._matchRoles = aRoles; - this._matchFunc = aMatchFunc; -} - -BaseTraversalRule.prototype = { - getMatchRoles: function BaseTraversalRule_getmatchRoles(aRules) { - aRules.value = this._matchRoles; - return aRules.value.length; - }, - - preFilter: Ci.nsIAccessibleTraversalRule.PREFILTER_DEFUNCT | - Ci.nsIAccessibleTraversalRule.PREFILTER_INVISIBLE, - - match: function BaseTraversalRule_match(aAccessible) - { - if (this._matchFunc) - return this._matchFunc(aAccessible); - - return Ci.nsIAccessibleTraversalRule.FILTER_MATCH; - }, - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIAccessibleTraversalRule]) -}; - -var TraversalRules = { - Simple: new BaseTraversalRule( - [Ci.nsIAccessibleRole.ROLE_MENUITEM, - Ci.nsIAccessibleRole.ROLE_LINK, - Ci.nsIAccessibleRole.ROLE_PAGETAB, - Ci.nsIAccessibleRole.ROLE_GRAPHIC, - // XXX: Find a better solution for ROLE_STATICTEXT. - // It allows to filter list bullets but at the same time it - // filters CSS generated content too as an unwanted side effect. - // Ci.nsIAccessibleRole.ROLE_STATICTEXT, - Ci.nsIAccessibleRole.ROLE_TEXT_LEAF, - Ci.nsIAccessibleRole.ROLE_PUSHBUTTON, - Ci.nsIAccessibleRole.ROLE_CHECKBUTTON, - Ci.nsIAccessibleRole.ROLE_RADIOBUTTON, - Ci.nsIAccessibleRole.ROLE_COMBOBOX, - Ci.nsIAccessibleRole.ROLE_PROGRESSBAR, - Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWN, - Ci.nsIAccessibleRole.ROLE_BUTTONMENU, - Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM, - Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT, - Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM, - Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON, - Ci.nsIAccessibleRole.ROLE_ENTRY], - function Simple_match(aAccessible) { - switch (aAccessible.role) { - case Ci.nsIAccessibleRole.ROLE_COMBOBOX: - // We don't want to ignore the subtree because this is often - // where the list box hangs out. - return Ci.nsIAccessibleTraversalRule.FILTER_MATCH; - case Ci.nsIAccessibleRole.ROLE_TEXT_LEAF: - { - // Nameless text leaves are boring, skip them. - let name = aAccessible.name; - if (name && name.trim()) - return Ci.nsIAccessibleTraversalRule.FILTER_MATCH; - else - return Ci.nsIAccessibleTraversalRule.FILTER_IGNORE; - } - case Ci.nsIAccessibleRole.ROLE_LINK: - // If the link has children we should land on them instead. - // Image map links don't have children so we need to match those. - if (aAccessible.childCount == 0) - return Ci.nsIAccessibleTraversalRule.FILTER_MATCH; - else - return Ci.nsIAccessibleTraversalRule.FILTER_IGNORE; - default: - // Ignore the subtree, if there is one. So that we don't land on - // the same content that was already presented by its parent. - return Ci.nsIAccessibleTraversalRule.FILTER_MATCH | - Ci.nsIAccessibleTraversalRule.FILTER_IGNORE_SUBTREE; - } - } - ), - - Anchor: new BaseTraversalRule( - [Ci.nsIAccessibleRole.ROLE_LINK], - function Anchor_match(aAccessible) - { - // We want to ignore links, only focus named anchors. - let state = {}; - let extraState = {}; - aAccessible.getState(state, extraState); - if (state.value & Ci.nsIAccessibleStates.STATE_LINKED) { - return Ci.nsIAccessibleTraversalRule.FILTER_IGNORE; - } else { - return Ci.nsIAccessibleTraversalRule.FILTER_MATCH; - } - }), - - Button: new BaseTraversalRule( - [Ci.nsIAccessibleRole.ROLE_PUSHBUTTON, - Ci.nsIAccessibleRole.ROLE_SPINBUTTON, - Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON, - Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWN, - Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWNGRID]), - - Combobox: new BaseTraversalRule( - [Ci.nsIAccessibleRole.ROLE_COMBOBOX, - Ci.nsIAccessibleRole.ROLE_LISTBOX]), - - Entry: new BaseTraversalRule( - [Ci.nsIAccessibleRole.ROLE_ENTRY, - Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT]), - - FormElement: new BaseTraversalRule( - [Ci.nsIAccessibleRole.ROLE_PUSHBUTTON, - Ci.nsIAccessibleRole.ROLE_SPINBUTTON, - Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON, - Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWN, - Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWNGRID, - Ci.nsIAccessibleRole.ROLE_COMBOBOX, - Ci.nsIAccessibleRole.ROLE_LISTBOX, - Ci.nsIAccessibleRole.ROLE_ENTRY, - Ci.nsIAccessibleRole.ROLE_PASSWORD_TEXT, - Ci.nsIAccessibleRole.ROLE_PAGETAB, - Ci.nsIAccessibleRole.ROLE_RADIOBUTTON, - Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM, - Ci.nsIAccessibleRole.ROLE_SLIDER, - Ci.nsIAccessibleRole.ROLE_CHECKBUTTON, - Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM]), - - Graphic: new BaseTraversalRule( - [Ci.nsIAccessibleRole.ROLE_GRAPHIC]), - - Heading: new BaseTraversalRule( - [Ci.nsIAccessibleRole.ROLE_HEADING]), - - ListItem: new BaseTraversalRule( - [Ci.nsIAccessibleRole.ROLE_LISTITEM, - Ci.nsIAccessibleRole.ROLE_TERM]), - - Link: new BaseTraversalRule( - [Ci.nsIAccessibleRole.ROLE_LINK], - function Link_match(aAccessible) - { - // We want to ignore anchors, only focus real links. - let state = {}; - let extraState = {}; - aAccessible.getState(state, extraState); - if (state.value & Ci.nsIAccessibleStates.STATE_LINKED) { - return Ci.nsIAccessibleTraversalRule.FILTER_MATCH; - } else { - return Ci.nsIAccessibleTraversalRule.FILTER_IGNORE; - } - }), - - List: new BaseTraversalRule( - [Ci.nsIAccessibleRole.ROLE_LIST, - Ci.nsIAccessibleRole.ROLE_DEFINITION_LIST]), - - PageTab: new BaseTraversalRule( - [Ci.nsIAccessibleRole.ROLE_PAGETAB]), - - RadioButton: new BaseTraversalRule( - [Ci.nsIAccessibleRole.ROLE_RADIOBUTTON, - Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM]), - - Separator: new BaseTraversalRule( - [Ci.nsIAccessibleRole.ROLE_SEPARATOR]), - - Table: new BaseTraversalRule( - [Ci.nsIAccessibleRole.ROLE_TABLE]), - - Checkbox: new BaseTraversalRule( - [Ci.nsIAccessibleRole.ROLE_CHECKBUTTON, - Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM]) -}; - -var VirtualCursorController = { - exploreByTouch: false, - editableState: 0, - - attach: function attach(aWindow) { - this.chromeWin = aWindow; - this.chromeWin.document.addEventListener('keypress', this, true); - this.chromeWin.addEventListener('mozAccessFuGesture', this, true); - }, - - detach: function detach() { - this.chromeWin.document.removeEventListener('keypress', this, true); - this.chromeWin.removeEventListener('mozAccessFuGesture', this, true); - }, - - handleEvent: function VirtualCursorController_handleEvent(aEvent) { - switch (aEvent.type) { - case 'keypress': - this._handleKeypress(aEvent); - break; - case 'mozAccessFuGesture': - this._handleGesture(aEvent); - break; - } - }, - - _handleGesture: function _handleGesture(aEvent) { - let document = Utils.getCurrentContentDoc(this.chromeWin); - let detail = aEvent.detail; - Logger.info('Gesture', detail.type, - '(fingers: ' + detail.touches.length + ')'); - - if (detail.touches.length == 1) { - switch (detail.type) { - case 'swiperight': - this.moveForward(document, aEvent.shiftKey); - break; - case 'swipeleft': - this.moveBackward(document, aEvent.shiftKey); - break; - case 'doubletap': - this.activateCurrent(document); - break; - case 'explore': - this.moveToPoint(document, detail.x, detail.y); - break; - } - } - - if (detail.touches.length == 3) { - switch (detail.type) { - case 'swiperight': - if (!Utils.scroll(this.chromeWin, -1, true)) - Utils.changePage(this.chromeWin, -1); - break; - case 'swipedown': - Utils.scroll(this.chromeWin, -1); - break; - case 'swipeleft': - if (!Utils.scroll(this.chromeWin, 1, true)) - Utils.changePage(this.chromeWin, 1); - case 'swipeup': - Utils.scroll(this.chromeWin, 1); - break; - } - } - }, - - _handleKeypress: function _handleKeypress(aEvent) { - let document = Utils.getCurrentContentDoc(this.chromeWin); - let target = aEvent.target; - - // Ignore keys with modifiers so the content could take advantage of them. - if (aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey) - return; - - switch (aEvent.keyCode) { - case 0: - // an alphanumeric key was pressed, handle it separately. - // If it was pressed with either alt or ctrl, just pass through. - // If it was pressed with meta, pass the key on without the meta. - if (this.editableState) - return; - - let key = String.fromCharCode(aEvent.charCode); - let methodName = '', rule = {}; - try { - [methodName, rule] = this.keyMap[key]; - } catch (x) { - return; - } - this[methodName](document, false, rule); - break; - case aEvent.DOM_VK_RIGHT: - if (this.editableState) { - if (target.selectionEnd != target.textLength) - // Don't move forward if caret is not at end of entry. - // XXX: Fix for rtl - return; - else - target.blur(); - } - this.moveForward(document, aEvent.shiftKey); - break; - case aEvent.DOM_VK_LEFT: - if (this.editableState) { - if (target.selectionEnd != 0) - // Don't move backward if caret is not at start of entry. - // XXX: Fix for rtl - return; - else - target.blur(); - } - this.moveBackward(document, aEvent.shiftKey); - break; - case aEvent.DOM_VK_UP: - if (this.editableState & Ci.nsIAccessibleStates.EXT_STATE_MULTI_LINE) { - if (target.selectionEnd != 0) - // Don't blur content if caret is not at start of text area. - return; - else - target.blur(); - } - - if (Utils.MozBuildApp == 'mobile/android') - // Return focus to native Android browser chrome. - Cc['@mozilla.org/android/bridge;1']. - getService(Ci.nsIAndroidBridge).handleGeckoMessage( - JSON.stringify({ gecko: { type: 'ToggleChrome:Focus' } })); - break; - case aEvent.DOM_VK_RETURN: - case aEvent.DOM_VK_ENTER: - if (this.editableState) - return; - this.activateCurrent(document); - break; - default: - return; - } - - aEvent.preventDefault(); - aEvent.stopPropagation(); - }, - - moveToPoint: function moveToPoint(aDocument, aX, aY) { - Utils.getVirtualCursor(aDocument).moveToPoint(TraversalRules.Simple, - aX, aY, true); - }, - - moveForward: function moveForward(aDocument, aLast, aRule) { - let virtualCursor = Utils.getVirtualCursor(aDocument); - if (aLast) { - virtualCursor.moveLast(TraversalRules.Simple); - } else { - try { - virtualCursor.moveNext(aRule || TraversalRules.Simple); - } catch (x) { - this.moveCursorToObject( - virtualCursor, - Utils.AccRetrieval.getAccessibleFor(aDocument.activeElement), aRule); - } - } - }, - - moveBackward: function moveBackward(aDocument, aFirst, aRule) { - let virtualCursor = Utils.getVirtualCursor(aDocument); - if (aFirst) { - virtualCursor.moveFirst(TraversalRules.Simple); - } else { - try { - virtualCursor.movePrevious(aRule || TraversalRules.Simple); - } catch (x) { - this.moveCursorToObject( - virtualCursor, - Utils.AccRetrieval.getAccessibleFor(aDocument.activeElement), aRule); - } - } - }, - - activateCurrent: function activateCurrent(document) { - let virtualCursor = Utils.getVirtualCursor(document); - let acc = virtualCursor.position; - - if (acc.actionCount > 0) { - acc.doAction(0); - } else { - // XXX Some mobile widget sets do not expose actions properly - // (via ARIA roles, etc.), so we need to generate a click. - // Could possibly be made simpler in the future. Maybe core - // engine could expose nsCoreUtiles::DispatchMouseEvent()? - let docAcc = Utils.AccRetrieval.getAccessibleFor(this.chromeWin.document); - let docX = {}, docY = {}, docW = {}, docH = {}; - docAcc.getBounds(docX, docY, docW, docH); - - let objX = {}, objY = {}, objW = {}, objH = {}; - acc.getBounds(objX, objY, objW, objH); - - let x = Math.round((objX.value - docX.value) + objW.value / 2); - let y = Math.round((objY.value - docY.value) + objH.value / 2); - - let cwu = this.chromeWin.QueryInterface(Ci.nsIInterfaceRequestor). - getInterface(Ci.nsIDOMWindowUtils); - cwu.sendMouseEventToWindow('mousedown', x, y, 0, 1, 0, false); - cwu.sendMouseEventToWindow('mouseup', x, y, 0, 1, 0, false); - } - }, - - moveCursorToObject: function moveCursorToObject(aVirtualCursor, - aAccessible, aRule) { - aVirtualCursor.moveNext(aRule || TraversalRules.Simple, aAccessible, true); - }, - - keyMap: { - a: ['moveForward', TraversalRules.Anchor], - A: ['moveBackward', TraversalRules.Anchor], - b: ['moveForward', TraversalRules.Button], - B: ['moveBackward', TraversalRules.Button], - c: ['moveForward', TraversalRules.Combobox], - C: ['moveBackward', TraversalRules.Combobox], - e: ['moveForward', TraversalRules.Entry], - E: ['moveBackward', TraversalRules.Entry], - f: ['moveForward', TraversalRules.FormElement], - F: ['moveBackward', TraversalRules.FormElement], - g: ['moveForward', TraversalRules.Graphic], - G: ['moveBackward', TraversalRules.Graphic], - h: ['moveForward', TraversalRules.Heading], - H: ['moveBackward', TraversalRules.Heading], - i: ['moveForward', TraversalRules.ListItem], - I: ['moveBackward', TraversalRules.ListItem], - k: ['moveForward', TraversalRules.Link], - K: ['moveBackward', TraversalRules.Link], - l: ['moveForward', TraversalRules.List], - L: ['moveBackward', TraversalRules.List], - p: ['moveForward', TraversalRules.PageTab], - P: ['moveBackward', TraversalRules.PageTab], - r: ['moveForward', TraversalRules.RadioButton], - R: ['moveBackward', TraversalRules.RadioButton], - s: ['moveForward', TraversalRules.Separator], - S: ['moveBackward', TraversalRules.Separator], - t: ['moveForward', TraversalRules.Table], - T: ['moveBackward', TraversalRules.Table], - x: ['moveForward', TraversalRules.Checkbox], - X: ['moveBackward', TraversalRules.Checkbox] - } -}; diff --git a/accessible/src/jsat/content-script.js b/accessible/src/jsat/content-script.js new file mode 100644 index 000000000000..86a14e9bc8a4 --- /dev/null +++ b/accessible/src/jsat/content-script.js @@ -0,0 +1,253 @@ +/* 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/. */ + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; + +Cu.import('resource://gre/modules/accessibility/Utils.jsm'); +Cu.import('resource://gre/modules/accessibility/EventManager.jsm'); +Cu.import('resource://gre/modules/accessibility/TraversalRules.jsm'); +Cu.import('resource://gre/modules/Services.jsm'); + +Logger.debug('content-script.js'); + +function virtualCursorControl(aMessage) { + if (Logger.logLevel >= Logger.DEBUG) + Logger.debug(aMessage.name, JSON.stringify(aMessage.json)); + + try { + let vc = Utils.getVirtualCursor(content.document); + let origin = aMessage.json.origin; + if (origin != 'child') { + if (forwardMessage(vc, aMessage)) + return; + } + + let details = aMessage.json; + let rule = TraversalRules[details.rule]; + let moved = 0; + switch (details.action) { + case 'moveFirst': + case 'moveLast': + moved = vc[details.action](rule); + break; + case 'moveNext': + case 'movePrevious': + try { + if (origin == 'parent' && vc.position == null) { + if (details.action == 'moveNext') + moved = vc.moveFirst(rule); + else + moved = vc.moveLast(rule); + } else { + moved = vc[details.action](rule); + } + } catch (x) { + moved = vc.moveNext(rule, content.document.activeElement, true); + } + break; + case 'moveToPoint': + moved = vc.moveToPoint(rule, details.x, details.y, true); + break; + case 'presentLastPivot': + EventManager.presentLastPivot(); + break; + default: + break; + } + + if (moved == true) { + forwardMessage(vc, aMessage); + } else if (moved == false && details.action != 'moveToPoint') { + if (origin == 'parent') { + vc.position = null; + } + aMessage.json.origin = 'child'; + sendAsyncMessage('AccessFu:VirtualCursor', aMessage.json); + } + } catch (x) { + Logger.error(x); + } +} + +function forwardMessage(aVirtualCursor, aMessage) { + try { + let acc = aVirtualCursor.position; + if (acc && acc.role == Ci.nsIAccessibleRole.ROLE_INTERNAL_FRAME) { + let mm = Utils.getMessageManager(acc.DOMNode); + mm.addMessageListener(aMessage.name, virtualCursorControl); + aMessage.json.origin = 'parent'; + // XXX: OOP content's screen offset is 0, + // so we remove the real screen offset here. + aMessage.json.x -= content.mozInnerScreenX; + aMessage.json.y -= content.mozInnerScreenY; + mm.sendAsyncMessage(aMessage.name, aMessage.json); + return true; + } + } catch (x) { + Logger.error(x); + } + return false; +} + +function activateCurrent(aMessage) { + Logger.debug('activateCurrent'); + function activateAccessible(aAccessible) { + if (aAccessible.actionCount > 0) { + aAccessible.doAction(0); + } else { + // XXX Some mobile widget sets do not expose actions properly + // (via ARIA roles, etc.), so we need to generate a click. + // Could possibly be made simpler in the future. Maybe core + // engine could expose nsCoreUtiles::DispatchMouseEvent()? + let docAcc = Utils.AccRetrieval.getAccessibleFor(content.document); + let docX = {}, docY = {}, docW = {}, docH = {}; + docAcc.getBounds(docX, docY, docW, docH); + + let objX = {}, objY = {}, objW = {}, objH = {}; + aAccessible.getBounds(objX, objY, objW, objH); + + let x = Math.round((objX.value - docX.value) + objW.value / 2); + let y = Math.round((objY.value - docY.value) + objH.value / 2); + + let cwu = content.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils); + cwu.sendMouseEventToWindow('mousedown', x, y, 0, 1, 0, false); + cwu.sendMouseEventToWindow('mouseup', x, y, 0, 1, 0, false); + } + } + + let vc = Utils.getVirtualCursor(content.document); + if (!forwardMessage(vc, aMessage)) + activateAccessible(vc.position); +} + +function scroll(aMessage) { + let vc = Utils.getVirtualCursor(content.document); + + function tryToScroll() { + let horiz = aMessage.json.horizontal; + let page = aMessage.json.page; + + // Search up heirarchy for scrollable element. + let acc = vc.position; + while (acc) { + let elem = acc.DOMNode; + + // We will do window scrolling next. + if (elem == content.document) + break; + + if (!horiz && elem.clientHeight < elem.scrollHeight) { + let s = content.getComputedStyle(elem); + if (s.overflowY == 'scroll' || s.overflowY == 'auto') { + elem.scrollTop += page * elem.clientHeight; + return true; + } + } + + if (horiz) { + if (elem.clientWidth < elem.scrollWidth) { + let s = content.getComputedStyle(elem); + if (s.overflowX == 'scroll' || s.overflowX == 'auto') { + elem.scrollLeft += page * elem.clientWidth; + return true; + } + } + + let controllers = acc. + getRelationByType( + Ci.nsIAccessibleRelation.RELATION_CONTROLLED_BY); + for (let i = 0; controllers.targetsCount > i; i++) { + let controller = controllers.getTarget(i); + // If the section has a controlling slider, it should be considered + // the page-turner. + if (controller.role == Ci.nsIAccessibleRole.ROLE_SLIDER) { + // Sliders are controlled with ctrl+right/left. I just decided :) + let evt = content.document.createEvent('KeyboardEvent'); + evt.initKeyEvent( + 'keypress', true, true, null, + true, false, false, false, + (page > 0) ? evt.DOM_VK_RIGHT : evt.DOM_VK_LEFT, 0); + controller.DOMNode.dispatchEvent(evt); + return true; + } + } + } + acc = acc.parent; + } + + // Scroll window. + if (!horiz && content.scrollMaxY && + ((page > 0 && content.scrollY < content.scrollMaxY) || + (page < 0 && content.scrollY > 0))) { + content.scroll(0, content.innerHeight); + return true; + } else if (horiz && content.scrollMaxX && + ((page > 0 && content.scrollX < content.scrollMaxX) || + (page < 0 && content.scrollX > 0))) { + content.scroll(content.innerWidth, 0); + return true; + } + + return false; + } + + if (aMessage.json.origin != 'child') { + if (forwardMessage(vc, aMessage)) + return; + } + + if (!tryToScroll()) { + // Failed to scroll anything in this document. Try in parent document. + aMessage.json.origin = 'child'; + sendAsyncMessage('AccessFu:Scroll', aMessage.json); + } +} + +addMessageListener('AccessFu:VirtualCursor', virtualCursorControl); +addMessageListener('AccessFu:Activate', activateCurrent); +addMessageListener('AccessFu:Scroll', scroll); + +addMessageListener( + 'AccessFu:Start', + function(m) { + if (m.json.buildApp) + Utils.MozBuildApp = m.json.buildApp; + + EventManager.start( + function sendMessage(aName, aDetails) { + sendAsyncMessage(aName, aDetails); + }); + + docShell.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebProgress). + addProgressListener(EventManager, + (Ci.nsIWebProgress.NOTIFY_STATE_ALL | + Ci.nsIWebProgress.NOTIFY_LOCATION)); + addEventListener('scroll', EventManager, true); + addEventListener('resize', EventManager, true); + // XXX: Ideally this would be an a11y event. Bug #742280. + addEventListener('DOMActivate', EventManager, true); + }); + +addMessageListener( + 'AccessFu:Stop', + function(m) { + Logger.debug('AccessFu:Stop'); + + EventManager.stop(); + + docShell.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebProgress). + removeProgressListener(EventManager); + removeEventListener('scroll', EventManager, true); + removeEventListener('resize', EventManager, true); + // XXX: Ideally this would be an a11y event. Bug #742280. + removeEventListener('DOMActivate', EventManager, true); + }); + +sendAsyncMessage('AccessFu:Ready'); diff --git a/accessible/src/jsat/jar.mn b/accessible/src/jsat/jar.mn index 3c77a61be838..aa989ff006cd 100644 --- a/accessible/src/jsat/jar.mn +++ b/accessible/src/jsat/jar.mn @@ -4,3 +4,4 @@ toolkit.jar: content/global/accessibility/AccessFu.css (AccessFu.css) + content/global/accessibility/content-script.js (content-script.js)