diff --git a/Makefile.in b/Makefile.in index a942284e511f..b747d4cbed95 100644 --- a/Makefile.in +++ b/Makefile.in @@ -177,7 +177,7 @@ EXTRA_BUILDID := -$(MOZ_SYMBOLS_EXTRA_BUILDID) endif SYMBOL_INDEX_NAME = \ - $(MOZ_APP_NAME)-$(MOZ_APP_VERSION)-$(OS_TARGET)-$(BUILDID)$(EXTRA_BUILDID)-symbols.txt + $(MOZ_APP_NAME)-$(MOZ_APP_VERSION)-$(OS_TARGET)-$(BUILDID)-$(CPU_ARCH)$(EXTRA_BUILDID)-symbols.txt buildsymbols: ifdef MOZ_CRASHREPORTER diff --git a/accessible/src/base/nsAccessiblePivot.cpp b/accessible/src/base/nsAccessiblePivot.cpp index 3e4d776bd2d1..c8393eda022e 100644 --- a/accessible/src/base/nsAccessiblePivot.cpp +++ b/accessible/src/base/nsAccessiblePivot.cpp @@ -42,6 +42,7 @@ #include "Accessible-inl.h" #include "nsAccUtils.h" #include "nsHyperTextAccessible.h" +#include "nsDocAccessible.h" #include "States.h" #include "nsArrayUtils.h" @@ -217,6 +218,10 @@ nsAccessiblePivot::MoveNext(nsIAccessibleTraversalRule* aRule, bool* aResult) NS_ENSURE_ARG(aResult); NS_ENSURE_ARG(aRule); + if (mPosition && (mPosition->IsDefunct() || + !mPosition->Document()->IsInDocument(mPosition))) + return NS_ERROR_NOT_IN_TREE; + nsresult rv = NS_OK; nsAccessible* accessible = SearchForward(mPosition, aRule, false, &rv); NS_ENSURE_SUCCESS(rv, rv); @@ -234,6 +239,10 @@ nsAccessiblePivot::MovePrevious(nsIAccessibleTraversalRule* aRule, bool* aResult NS_ENSURE_ARG(aResult); NS_ENSURE_ARG(aRule); + if (mPosition && (mPosition->IsDefunct() || + !mPosition->Document()->IsInDocument(mPosition))) + return NS_ERROR_NOT_IN_TREE; + nsresult rv = NS_OK; nsAccessible* accessible = SearchBackward(mPosition, aRule, false, &rv); NS_ENSURE_SUCCESS(rv, rv); @@ -250,6 +259,10 @@ nsAccessiblePivot::MoveFirst(nsIAccessibleTraversalRule* aRule, bool* aResult) { NS_ENSURE_ARG(aResult); NS_ENSURE_ARG(aRule); + + if (mRoot && mRoot->IsDefunct()) + return NS_ERROR_NOT_IN_TREE; + nsresult rv = NS_OK; nsAccessible* accessible = SearchForward(mRoot, aRule, true, &rv); NS_ENSURE_SUCCESS(rv, rv); @@ -267,6 +280,9 @@ nsAccessiblePivot::MoveLast(nsIAccessibleTraversalRule* aRule, bool* aResult) NS_ENSURE_ARG(aResult); NS_ENSURE_ARG(aRule); + if (mRoot && mRoot->IsDefunct()) + return NS_ERROR_NOT_IN_TREE; + *aResult = false; nsresult rv = NS_OK; nsAccessible* lastAccessible = mRoot; @@ -334,6 +350,9 @@ nsAccessiblePivot::RemoveObserver(nsIAccessiblePivotObserver* aObserver) bool nsAccessiblePivot::IsRootDescendant(nsAccessible* aAccessible) { + if (!mRoot || mRoot->IsDefunct()) + return false; + nsAccessible* accessible = aAccessible; do { if (accessible == mRoot) diff --git a/accessible/src/base/nsAccessiblePivot.h b/accessible/src/base/nsAccessiblePivot.h index 2f1e58ec3437..0340e56fc10b 100644 --- a/accessible/src/base/nsAccessiblePivot.h +++ b/accessible/src/base/nsAccessiblePivot.h @@ -49,6 +49,10 @@ class nsAccessible; class nsIAccessibleTraversalRule; +// raised when current pivot's position is needed but it is not in the tree. +#define NS_ERROR_NOT_IN_TREE \ +NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_GENERAL, 0x26) + /** * Class represents an accessible pivot. */ diff --git a/accessible/src/html/nsHTMLSelectAccessible.cpp b/accessible/src/html/nsHTMLSelectAccessible.cpp index 10ebb25c76a4..1de97dee37e8 100644 --- a/accessible/src/html/nsHTMLSelectAccessible.cpp +++ b/accessible/src/html/nsHTMLSelectAccessible.cpp @@ -88,9 +88,6 @@ nsHTMLSelectListAccessible::NativeState() role nsHTMLSelectListAccessible::NativeRole() { - if (mParent && mParent->Role() == roles::COMBOBOX) - return roles::COMBOBOX_LIST; - return roles::LISTBOX; } @@ -710,6 +707,12 @@ nsHTMLComboboxListAccessible::IsPrimaryForNode() const //////////////////////////////////////////////////////////////////////////////// // nsHTMLComboboxAccessible: nsAccessible +role +nsHTMLComboboxListAccessible::NativeRole() +{ + return roles::COMBOBOX_LIST; +} + PRUint64 nsHTMLComboboxListAccessible::NativeState() { diff --git a/accessible/src/html/nsHTMLSelectAccessible.h b/accessible/src/html/nsHTMLSelectAccessible.h index 1998e3704c38..69b6ebdf42a5 100644 --- a/accessible/src/html/nsHTMLSelectAccessible.h +++ b/accessible/src/html/nsHTMLSelectAccessible.h @@ -258,6 +258,7 @@ public: virtual bool IsPrimaryForNode() const; // nsAccessible + virtual mozilla::a11y::role NativeRole(); virtual PRUint64 NativeState(); virtual void GetBoundsRect(nsRect& aBounds, nsIFrame** aBoundingFrame); diff --git a/accessible/src/jsat/AccessFu.jsm b/accessible/src/jsat/AccessFu.jsm index 996a351f2f37..fa4f7f0d7e69 100644 --- a/accessible/src/jsat/AccessFu.jsm +++ b/accessible/src/jsat/AccessFu.jsm @@ -48,14 +48,14 @@ var AccessFu = { } catch (x) { } - this.processPreferences(accessPref); + this._processPreferences(accessPref); }, /** * Start AccessFu mode, this primarily means controlling the virtual cursor * with arrow keys. */ - enable: function enable() { + _enable: function _enable() { if (this._enabled) return; this._enabled = true; @@ -79,7 +79,7 @@ var AccessFu = { /** * Disable AccessFu and return to default interaction mode. */ - disable: function disable() { + _disable: function _disable() { if (!this._enabled) return; this._enabled = false; @@ -98,7 +98,7 @@ var AccessFu = { this.chromeWin.removeEventListener('TabOpen', this, true); }, - processPreferences: function processPreferences(aPref) { + _processPreferences: function _processPreferences(aPref) { if (Services.appinfo.OS == 'Android') { if (aPref == ACCESSFU_AUTO) { if (!this._observingSystemSettings) { @@ -118,9 +118,9 @@ var AccessFu = { } if (aPref == ACCESSFU_ENABLE) - this.enable(); + this._enable(); else - this.disable(); + this._disable(); }, addPresenter: function addPresenter(presenter) { @@ -171,19 +171,19 @@ var AccessFu = { switch (aTopic) { case 'Accessibility:Settings': if (JSON.parse(aData).enabled) - this.enable(); + this._enable(); else - this.disable(); + this._disable(); break; case 'nsPref:changed': if (aData == 'accessfu') - this.processPreferences(this.prefsBranch.getIntPref('accessfu')); + this._processPreferences(this.prefsBranch.getIntPref('accessfu')); break; case 'accessible-event': let event; try { event = aSubject.QueryInterface(Ci.nsIAccessibleEvent); - this.handleAccEvent(event); + this._handleAccEvent(event); } catch (ex) { dump(ex); return; @@ -191,7 +191,7 @@ var AccessFu = { } }, - handleAccEvent: function handleAccEvent(aEvent) { + _handleAccEvent: function _handleAccEvent(aEvent) { switch (aEvent.eventType) { case Ci.nsIAccessibleEvent.EVENT_VIRTUALCURSOR_CHANGED: { @@ -199,6 +199,21 @@ var AccessFu = { QueryInterface(Ci.nsIAccessibleCursorable).virtualCursor; let event = aEvent. QueryInterface(Ci.nsIAccessibleVirtualCursorChangeEvent); + let position = pivot.position; + let doc = aEvent.DOMNode; + + if (doc instanceof Ci.nsIDOMDocument && position.DOMNode) { + // Set the caret to the start of the pivot position, and move + // the focus in the same manner as browse with caret mode. + // This blurs the focus on the previous pivot position (if it + // was activated), and keeps us in a predictable spot for tab + // focus. + let sel = doc.getSelection(); + sel.collapse(position.DOMNode, 0); + Cc["@mozilla.org/focus-manager;1"] + .getService(Ci.nsIFocusManager).moveFocus( + doc.defaultView, null, Ci.nsIFocusManager.MOVEFOCUS_CARET, 0); + } let newContext = this.getNewContext(event.oldAccessible, pivot.position); @@ -251,13 +266,13 @@ var AccessFu = { let state = {}; docAcc.getState(state, {}); if (state.value & Ci.nsIAccessibleStates.STATE_BUSY && - this.isNotChromeDoc(docAcc)) + this._isNotChromeDoc(docAcc)) this.presenters.forEach( function(p) { p.tabStateChanged(docAcc, 'loading'); } ); delete this._pendingDocuments[aEvent.DOMNode]; } - if (this.isBrowserDoc(docAcc)) + if (this._isBrowserDoc(docAcc)) // A new top-level content document has been attached this.presenters.forEach( function(p) { p.tabStateChanged(docAcc, 'newdoc'); } @@ -267,7 +282,7 @@ var AccessFu = { } case Ci.nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE: { - if (this.isNotChromeDoc(aEvent.accessible)) { + if (this._isNotChromeDoc(aEvent.accessible)) { this.presenters.forEach( function(p) { p.tabStateChanged(aEvent.accessible, 'loaded'); @@ -296,7 +311,7 @@ var AccessFu = { } case Ci.nsIAccessibleEvent.EVENT_FOCUS: { - if (this.isBrowserDoc(aEvent.accessible)) { + if (this._isBrowserDoc(aEvent.accessible)) { // The document recieved focus, call tabSelected to present current tab. this.presenters.forEach( function(p) { p.tabSelected(aEvent.accessible); }); @@ -342,7 +357,7 @@ var AccessFu = { * @param {nsIAccessible} aDocAcc the accessible to check. * @return {boolean} true if this is a top-level content document. */ - isBrowserDoc: function isBrowserDoc(aDocAcc) { + _isBrowserDoc: function _isBrowserDoc(aDocAcc) { let parent = aDocAcc.parent; if (!parent) return false; @@ -360,7 +375,7 @@ var AccessFu = { * @param {nsIDOMDocument} aDocument the document to check. * @return {boolean} true if this is not a chrome document. */ - isNotChromeDoc: function isNotChromeDoc(aDocument) { + _isNotChromeDoc: function _isNotChromeDoc(aDocument) { let location = aDocument.DOMNode.location; if (!location) return false; diff --git a/accessible/src/jsat/Presenters.jsm b/accessible/src/jsat/Presenters.jsm index 28471ec7ef9e..5ff5e8f453fb 100644 --- a/accessible/src/jsat/Presenters.jsm +++ b/accessible/src/jsat/Presenters.jsm @@ -52,7 +52,9 @@ Presenter.prototype = { /** * Text has changed, either by the user or by the system. TODO. */ - textChanged: function textChanged(aIsInserted, aStartOffset, aLength, aText, aModifiedText) {}, + textChanged: function textChanged(aIsInserted, aStartOffset, + aLength, aText, + aModifiedText) {}, /** * Text selection has changed. TODO. @@ -95,228 +97,236 @@ Presenter.prototype = { function VisualPresenter() {} -VisualPresenter.prototype = new Presenter(); +VisualPresenter.prototype = { + __proto__: Presenter.prototype, -/** - * The padding in pixels between the object and the highlight border. - */ -VisualPresenter.prototype.BORDER_PADDING = 2; + /** + * The padding in pixels between the object and the highlight border. + */ + BORDER_PADDING: 2, -VisualPresenter.prototype.attach = function(aWindow) { - this.chromeWin = aWindow; + attach: function VisualPresenter_attach(aWindow) { + this.chromeWin = aWindow; - // Add stylesheet - let stylesheetURL = 'chrome://global/content/accessibility/AccessFu.css'; - this.stylesheet = aWindow.document.createProcessingInstruction( - 'xml-stylesheet', 'href="' + stylesheetURL + '" type="text/css"'); - aWindow.document.insertBefore(this.stylesheet, aWindow.document.firstChild); + // Add stylesheet + let stylesheetURL = 'chrome://global/content/accessibility/AccessFu.css'; + this.stylesheet = aWindow.document.createProcessingInstruction( + 'xml-stylesheet', 'href="' + stylesheetURL + '" type="text/css"'); + aWindow.document.insertBefore(this.stylesheet, aWindow.document.firstChild); - // Add highlight box - this.highlightBox = this.chromeWin.document. - createElementNS('http://www.w3.org/1999/xhtml', 'div'); - this.chromeWin.document.documentElement.appendChild(this.highlightBox); - this.highlightBox.id = 'virtual-cursor-box'; + // Add highlight 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'; + // 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); -}; + this.highlightBox.appendChild(inset); + }, -VisualPresenter.prototype.detach = function() { - this.chromeWin.document.removeChild(this.stylesheet); - this.highlightBox.parentNode.removeChild(this.highlightBox); - this.highlightBox = this.stylesheet = null; -}; + detach: function VisualPresenter_detach() { + this.chromeWin.document.removeChild(this.stylesheet); + this.highlightBox.parentNode.removeChild(this.highlightBox); + this.highlightBox = this.stylesheet = null; + }, -VisualPresenter.prototype.viewportChanged = function() { - if (this._currentObject) - this.highlight(this._currentObject); -}; + viewportChanged: function VisualPresenter_viewportChanged() { + if (this._currentObject) + this._highlight(this._currentObject); + }, -VisualPresenter.prototype.pivotChanged = function(aObject, aNewContext) { - this._currentObject = aObject; + pivotChanged: function VisualPresenter_pivotChanged(aObject, aNewContext) { + this._currentObject = aObject; - if (!aObject) { - this.hide(); - return; + if (!aObject) { + this._hide(); + return; + } + + try { + aObject.scrollTo(Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE); + this._highlight(aObject); + } catch (e) { + dump('Error getting bounds: ' + e); + return; + } + }, + + tabSelected: function VisualPresenter_tabSelected(aDocObj) { + let vcPos = aDocObj ? aDocObj.QueryInterface(Ci.nsIAccessibleCursorable). + virtualCursor.position : null; + + this.pivotChanged(vcPos); + }, + + tabStateChanged: function VisualPresenter_tabStateChanged(aDocObj, + aPageState) { + if (aPageState == 'newdoc') + this.pivotChanged(null); + }, + + // Internals + + _hide: function _hide() { + this.highlightBox.style.display = 'none'; + }, + + _highlight: function _highlight(aObject) { + let vp = (Services.appinfo.OS == 'Android') ? + this.chromeWin.BrowserApp.selectedTab.getViewport() : + { zoom: 1.0, offsetY: 0 }; + + let bounds = this._getBounds(aObject, vp.zoom); + + // First hide it to avoid flickering when changing the style. + this.highlightBox.style.display = 'none'; + this.highlightBox.style.top = bounds.top + 'px'; + this.highlightBox.style.left = bounds.left + 'px'; + this.highlightBox.style.width = bounds.width + 'px'; + this.highlightBox.style.height = bounds.height + 'px'; + this.highlightBox.style.display = 'block'; + }, + + _getBounds: function _getBounds(aObject, aZoom, aStart, aEnd) { + let objX = {}, objY = {}, objW = {}, objH = {}; + + if (aEnd >= 0 && aStart >= 0 && aEnd != aStart) { + // TODO: Get bounds for text ranges. Leaving this blank until we have + // proper text navigation in the virtual cursor. + } + + aObject.getBounds(objX, objY, objW, objH); + + // Can't specify relative coords in nsIAccessible.getBounds, so we do it. + let docX = {}, docY = {}; + let docRoot = aObject.rootDocument.QueryInterface(Ci.nsIAccessible); + docRoot.getBounds(docX, docY, {}, {}); + + let rv = { + left: Math.round((objX.value - docX.value - this.BORDER_PADDING) * aZoom), + top: Math.round((objY.value - docY.value - this.BORDER_PADDING) * aZoom), + width: Math.round((objW.value + (this.BORDER_PADDING * 2)) * aZoom), + height: Math.round((objH.value + (this.BORDER_PADDING * 2)) * aZoom) + }; + + return rv; } - - try { - aObject.scrollTo(Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE); - this.highlight(aObject); - } catch (e) { - dump('Error getting bounds: ' + e); - return; - } -}; - -VisualPresenter.prototype.tabSelected = function(aDocObj) { - let vcPos = aDocObj ? - aDocObj.QueryInterface(Ci.nsIAccessibleCursorable).virtualCursor.position : - null; - - this.pivotChanged(vcPos); -}; - -VisualPresenter.prototype.tabStateChanged = function(aDocObj, aPageState) { - if (aPageState == "newdoc") - this.pivotChanged(null); -}; - -// Internals - -VisualPresenter.prototype.hide = function hide() { - this.highlightBox.style.display = 'none'; -}; - -VisualPresenter.prototype.highlight = function(aObject) { - let vp = (Services.appinfo.OS == 'Android') ? - this.chromeWin.BrowserApp.selectedTab.getViewport() : - { zoom: 1.0, offsetY: 0 }; - - let bounds = this.getBounds(aObject, vp.zoom); - - // First hide it to avoid flickering when changing the style. - this.highlightBox.style.display = 'none'; - this.highlightBox.style.top = bounds.top + 'px'; - this.highlightBox.style.left = bounds.left + 'px'; - this.highlightBox.style.width = bounds.width + 'px'; - this.highlightBox.style.height = bounds.height + 'px'; - this.highlightBox.style.display = 'block'; -}; - -VisualPresenter.prototype.getBounds = function(aObject, aZoom, aStart, aEnd) { - let objX = {}, objY = {}, objW = {}, objH = {}; - - if (aEnd >= 0 && aStart >= 0 && aEnd != aStart) { - // TODO: Get bounds for text ranges. Leaving this blank until we have - // proper text navigation in the virtual cursor. - } - - aObject.getBounds(objX, objY, objW, objH); - - // Can't specify relative coords in nsIAccessible.getBounds, so we do it. - let docX = {}, docY = {}; - let docRoot = aObject.rootDocument.QueryInterface(Ci.nsIAccessible); - docRoot.getBounds(docX, docY, {}, {}); - - let rv = { - left: Math.round((objX.value - docX.value - this.BORDER_PADDING) * aZoom), - top: Math.round((objY.value - docY.value - this.BORDER_PADDING) * aZoom), - width: Math.round((objW.value + (this.BORDER_PADDING * 2)) * aZoom), - height: Math.round((objH.value + (this.BORDER_PADDING * 2)) * aZoom) - }; - - return rv; }; /** * Android presenter. Fires Android a11y events. */ -const ANDROID_TYPE_VIEW_CLICKED = 0x01; -const ANDROID_TYPE_VIEW_LONG_CLICKED = 0x02; -const ANDROID_TYPE_VIEW_SELECTED = 0x04; -const ANDROID_TYPE_VIEW_FOCUSED = 0x08; -const ANDROID_TYPE_VIEW_TEXT_CHANGED = 0x10; -const ANDROID_TYPE_WINDOW_STATE_CHANGED = 0x20; - function AndroidPresenter() {} -AndroidPresenter.prototype = new Presenter(); +AndroidPresenter.prototype = { + __proto__: Presenter.prototype, + + // Android AccessibilityEvent type constants. + ANDROID_VIEW_CLICKED: 0x01, + ANDROID_VIEW_LONG_CLICKED: 0x02, + ANDROID_VIEW_SELECTED: 0x04, + ANDROID_VIEW_FOCUSED: 0x08, + ANDROID_VIEW_TEXT_CHANGED: 0x10, + ANDROID_WINDOW_STATE_CHANGED: 0x20, + + pivotChanged: function AndroidPresenter_pivotChanged(aObject, aNewContext) { + let output = []; + for (let i in aNewContext) + output.push.apply(output, + UtteranceGenerator.genForObject(aNewContext[i])); -AndroidPresenter.prototype.pivotChanged = function(aObject, aNewContext) { - let output = []; - for (let i in aNewContext) output.push.apply(output, - UtteranceGenerator.genForObject(aNewContext[i])); + UtteranceGenerator.genForObject(aObject, true)); - output.push.apply(output, - UtteranceGenerator.genForObject(aObject, true)); + this.sendMessageToJava({ + gecko: { + type: 'Accessibility:Event', + eventType: this.ANDROID_VIEW_FOCUSED, + text: output + } + }); + }, - this.sendMessageToJava({ - gecko: { - type: 'Accessibility:Event', - eventType: ANDROID_TYPE_VIEW_FOCUSED, - text: output + actionInvoked: function AndroidPresenter_actionInvoked(aObject, aActionName) { + this.sendMessageToJava({ + gecko: { + type: 'Accessibility:Event', + eventType: this.ANDROID_VIEW_CLICKED, + text: UtteranceGenerator.genForAction(aObject, aActionName) + } + }); + }, + + tabSelected: function AndroidPresenter_tabSelected(aDocObj) { + // Send a pivot change message with the full context utterance for this doc. + let vcDoc = aDocObj.QueryInterface(Ci.nsIAccessibleCursorable); + let context = []; + + let parent = vcDoc.virtualCursor.position || aDocObj; + while ((parent = parent.parent)) { + context.push(parent); + if (parent == aDocObj) + break; } - }); -}; -AndroidPresenter.prototype.actionInvoked = function(aObject, aActionName) { - this.sendMessageToJava({ - gecko: { + context.reverse(); + + this.pivotChanged(vcDoc.virtualCursor.position || aDocObj, context); + }, + + tabStateChanged: function AndroidPresenter_tabStateChanged(aDocObj, + aPageState) { + let stateUtterance = UtteranceGenerator. + genForTabStateChange(aDocObj, aPageState); + + if (!stateUtterance.length) + return; + + this.sendMessageToJava({ + gecko: { + type: 'Accessibility:Event', + eventType: this.ANDROID_VIEW_TEXT_CHANGED, + text: stateUtterance, + addedCount: stateUtterance.join(' ').length, + removedCount: 0, + fromIndex: 0 + } + }); + }, + + textChanged: function AndroidPresenter_textChanged(aIsInserted, aStart, + aLength, aText, + aModifiedText) { + let androidEvent = { type: 'Accessibility:Event', - eventType: ANDROID_TYPE_VIEW_CLICKED, - text: UtteranceGenerator.genForAction(aObject, aActionName) + eventType: this.ANDROID_VIEW_TEXT_CHANGED, + text: [aText], + fromIndex: aStart + }; + + if (aIsInserted) { + androidEvent.addedCount = aLength; + androidEvent.beforeText = + aText.substring(0, aStart) + aText.substring(aStart + aLength); + } else { + androidEvent.removedCount = aLength; + androidEvent.beforeText = + aText.substring(0, aStart) + aModifiedText + aText.substring(aStart); } - }); -}; -AndroidPresenter.prototype.tabSelected = function(aDocObj) { - // Send a pivot change message with the full context utterance for this doc. - let vcDoc = aDocObj.QueryInterface(Ci.nsIAccessibleCursorable); - let context = []; + this.sendMessageToJava({gecko: androidEvent}); + }, - let parent = vcDoc.virtualCursor.position || aDocObj; - while ((parent = parent.parent)) { - context.push(parent); - if (parent == aDocObj) - break; + sendMessageToJava: function AndroidPresenter_sendMessageTojava(aMessage) { + return Cc['@mozilla.org/android/bridge;1']. + getService(Ci.nsIAndroidBridge). + handleGeckoMessage(JSON.stringify(aMessage)); } - - context.reverse(); - - this.pivotChanged(vcDoc.virtualCursor.position || aDocObj, context); -}; - -AndroidPresenter.prototype.tabStateChanged = function(aDocObj, aPageState) { - let stateUtterance = UtteranceGenerator. - genForTabStateChange(aDocObj, aPageState); - - if (!stateUtterance.length) - return; - - this.sendMessageToJava({ - gecko: { - type: 'Accessibility:Event', - eventType: ANDROID_TYPE_VIEW_TEXT_CHANGED, - text: stateUtterance, - addedCount: stateUtterance.join(' ').length, - removedCount: 0, - fromIndex: 0 - } - }); -}; - -AndroidPresenter.prototype.textChanged = function(aIsInserted, aStart, aLength, aText, aModifiedText) { - let androidEvent = { - type: 'Accessibility:Event', - eventType: ANDROID_TYPE_VIEW_TEXT_CHANGED, - text: [aText], - fromIndex: aStart - }; - - if (aIsInserted) { - androidEvent.addedCount = aLength; - androidEvent.beforeText = - aText.substring(0, aStart) + aText.substring(aStart + aLength); - } else { - androidEvent.removedCount = aLength; - androidEvent.beforeText = - aText.substring(0, aStart) + aModifiedText + aText.substring(aStart); - } - - this.sendMessageToJava({gecko: androidEvent}); -}; - -AndroidPresenter.prototype.sendMessageToJava = function(aMessage) { - return Cc['@mozilla.org/android/bridge;1']. - getService(Ci.nsIAndroidBridge). - handleGeckoMessage(JSON.stringify(aMessage)); }; /** @@ -325,8 +335,10 @@ AndroidPresenter.prototype.sendMessageToJava = function(aMessage) { function DummyAndroidPresenter() {} -DummyAndroidPresenter.prototype = new AndroidPresenter(); +DummyAndroidPresenter.prototype = { + __proto__: AndroidPresenter.prototype, -DummyAndroidPresenter.prototype.sendMessageToJava = function(aMessage) { - dump(JSON.stringify(aMessage, null, 2) + '\n'); + sendMessageToJava: function DummyAndroidPresenter_sendMessageToJava(aMsg) { + dump(JSON.stringify(aMsg, null, 2) + '\n'); + } }; diff --git a/accessible/src/jsat/UtteranceGenerator.jsm b/accessible/src/jsat/UtteranceGenerator.jsm index 8f3f0f6248dd..5db4489021d4 100644 --- a/accessible/src/jsat/UtteranceGenerator.jsm +++ b/accessible/src/jsat/UtteranceGenerator.jsm @@ -65,10 +65,10 @@ var UtteranceGenerator = { * @return {Array} Two string array. The first string describes the object * and its states. The second string is the object's name. Some object * types may have the description or name omitted, instead an empty string - * is returned as a placeholder. Whether the object's description or it's role - * is included is determined by {@link verbosityRoleMap}. + * is returned as a placeholder. Whether the object's description or it's + * role is included is determined by {@link verbosityRoleMap}. */ - genForObject: function(aAccessible, aForceName) { + genForObject: function genForObject(aAccessible, aForceName) { let roleString = gAccRetrieval.getStringRole(aAccessible.role); let func = this.objectUtteranceFunctions[roleString] || @@ -91,7 +91,7 @@ var UtteranceGenerator = { * {@link gActionMap}. * @return {Array} A one string array with the action. */ - genForAction: function(aObject, aActionName) { + genForAction: function genForAction(aObject, aActionName) { return [gStringBundle.GetStringFromName(this.gActionMap[aActionName])]; }, @@ -103,7 +103,7 @@ var UtteranceGenerator = { * {@link Presenter.tabStateChanged}. * @return {Array} The tab state utterace. */ - genForTabStateChange: function (aObject, aTabState) { + genForTabStateChange: function genForTabStateChange(aObject, aTabState) { switch (aTabState) { case 'newtab': return [gStringBundle.GetStringFromName('tabNew')]; @@ -177,7 +177,8 @@ var UtteranceGenerator = { objectUtteranceFunctions: { defaultFunc: function defaultFunc(aAccessible, aRoleStr, aFlags) { let name = (aFlags & INCLUDE_NAME) ? (aAccessible.name || '') : ''; - let desc = (aFlags & INCLUDE_ROLE) ? this._getLocalizedRole(aRoleStr) : ''; + let desc = (aFlags & INCLUDE_ROLE) ? + this._getLocalizedRole(aRoleStr) : ''; let utterance = []; @@ -207,7 +208,7 @@ var UtteranceGenerator = { return utterance; }, - heading: function(aAccessible, aRoleStr, aFlags) { + heading: function heading(aAccessible, aRoleStr, aFlags) { let name = (aFlags & INCLUDE_NAME) ? (aAccessible.name || '') : ''; let level = {}; aAccessible.groupPosition(level, {}, {}); @@ -220,7 +221,7 @@ var UtteranceGenerator = { return utterance; }, - listitem: function(aAccessible, aRoleStr, aFlags) { + listitem: function listitem(aAccessible, aRoleStr, aFlags) { let name = (aFlags & INCLUDE_NAME) ? (aAccessible.name || '') : ''; let localizedRole = this._getLocalizedRole(aRoleStr); let itemno = {}; diff --git a/accessible/src/jsat/VirtualCursorController.jsm b/accessible/src/jsat/VirtualCursorController.jsm index 9d0fe5c88a5f..6f3d5638a587 100644 --- a/accessible/src/jsat/VirtualCursorController.jsm +++ b/accessible/src/jsat/VirtualCursorController.jsm @@ -18,16 +18,20 @@ var gAccRetrieval = Cc['@mozilla.org/accessibleRetrieval;1']. getService(Ci.nsIAccessibleRetrieval); var VirtualCursorController = { + NOT_EDITABLE: 0, + SINGLE_LINE_EDITABLE: 1, + MULTI_LINE_EDITABLE: 2, + attach: function attach(aWindow) { this.chromeWin = aWindow; - this.chromeWin.document.addEventListener('keypress', this.onkeypress, true); + this.chromeWin.document.addEventListener('keypress', this, true); }, detach: function detach() { - this.chromeWin.document.removeEventListener('keypress', this.onkeypress, true); + this.chromeWin.document.removeEventListener('keypress', this, true); }, - getBrowserApp: function getBrowserApp() { + _getBrowserApp: function _getBrowserApp() { switch (Services.appinfo.OS) { case 'Android': return this.chromeWin.BrowserApp; @@ -36,39 +40,49 @@ var VirtualCursorController = { } }, - onkeypress: function onkeypress(aEvent) { - let document = VirtualCursorController.getBrowserApp(). - selectedBrowser.contentDocument; - - dump('keypress ' + aEvent.keyCode + '\n'); + handleEvent: function handleEvent(aEvent) { + let document = this._getBrowserApp().selectedBrowser.contentDocument; + let target = aEvent.target; switch (aEvent.keyCode) { case aEvent.DOM_VK_END: - VirtualCursorController.moveForward(document, true); + this.moveForward(document, true); break; case aEvent.DOM_VK_HOME: - VirtualCursorController.moveBackward(document, true); + this.moveBackward(document, true); break; case aEvent.DOM_VK_RIGHT: - VirtualCursorController.moveForward(document, aEvent.shiftKey); + if (this._isEditableText(target) && + target.selectionEnd != target.textLength) + // Don't move forward if caret is not at end of entry. + // XXX: Fix for rtl + return; + this.moveForward(document, aEvent.shiftKey); break; case aEvent.DOM_VK_LEFT: - VirtualCursorController.moveBackward(document, aEvent.shiftKey); + if (this._isEditableText(target) && + target.selectionEnd != 0) + // Don't move backward if caret is not at start of entry. + // XXX: Fix for rtl + return; + this.moveBackward(document, aEvent.shiftKey); break; case aEvent.DOM_VK_UP: + if (this._isEditableText(target) == this.MULTI_LINE_EDITABLE && + target.selectionEnd != 0) + // Don't blur content if caret is not at start of text area. + return; if (Services.appinfo.OS == 'Android') - // Return focus to browser chrome, which in Android is a native widget. + // 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: - // XXX: It is true that desktop does not map the keypad enter key to - // DOM_VK_ENTER. So for desktop we require a ctrl+return instead. - if (Services.appinfo.OS == 'Android' || !aEvent.ctrlKey) - return; case aEvent.DOM_VK_ENTER: - VirtualCursorController.activateCurrent(document); + if (this._isEditableText(target)) + return; + this.activateCurrent(document); break; default: return; @@ -78,6 +92,18 @@ var VirtualCursorController = { aEvent.stopPropagation(); }, + _isEditableText: function _isEditableText(aElement) { + // XXX: Support contentEditable and design mode + if (aElement instanceof Ci.nsIDOMHTMLInputElement && + aElement.mozIsTextField(false)) + return this.SINGLE_LINE_EDITABLE; + + if (aElement instanceof Ci.nsIDOMHTMLTextAreaElement) + return this.MULTI_LINE_EDITABLE; + + return this.NOT_EDITABLE; + }, + moveForward: function moveForward(document, last) { let virtualCursor = this.getVirtualCursor(document); if (last) { @@ -110,7 +136,7 @@ var VirtualCursorController = { }, SimpleTraversalRule: { - getMatchRoles: function(aRules) { + getMatchRoles: function SimpleTraversalRule_getmatchRoles(aRules) { aRules.value = this._matchRoles; return this._matchRoles.length; }, @@ -118,7 +144,7 @@ var VirtualCursorController = { preFilter: Ci.nsIAccessibleTraversalRule.PREFILTER_DEFUNCT | Ci.nsIAccessibleTraversalRule.PREFILTER_INVISIBLE, - match: function(aAccessible) { + match: function SimpleTraversalRule_match(aAccessible) { switch (aAccessible.role) { case Ci.nsIAccessibleRole.ROLE_COMBOBOX: // We don't want to ignore the subtree because this is often diff --git a/accessible/tests/mochitest/pivot.js b/accessible/tests/mochitest/pivot.js index 86802373908f..cfb9c4a47c5d 100644 --- a/accessible/tests/mochitest/pivot.js +++ b/accessible/tests/mochitest/pivot.js @@ -8,6 +8,8 @@ const FILTER_MATCH = nsIAccessibleTraversalRule.FILTER_MATCH; const FILTER_IGNORE = nsIAccessibleTraversalRule.FILTER_IGNORE; const FILTER_IGNORE_SUBTREE = nsIAccessibleTraversalRule.FILTER_IGNORE_SUBTREE; +const NS_ERROR_NOT_IN_TREE = 0x80780026; + //////////////////////////////////////////////////////////////////////////////// // Traversal rules @@ -68,13 +70,13 @@ var ObjectTraversalRule = /** * A checker for virtual cursor changed events. */ -function virtualCursorChangedChecker(aDocAcc, aIdOrNameOrAcc, aTextOffsets) +function VCChangedChecker(aDocAcc, aIdOrNameOrAcc, aTextOffsets) { this.__proto__ = new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc); - this.check = function virtualCursorChangedChecker_check(aEvent) + this.check = function VCChangedChecker_check(aEvent) { - SimpleTest.info("virtualCursorChangedChecker_check"); + SimpleTest.info("VCChangedChecker_check"); var event = null; try { @@ -100,7 +102,7 @@ function virtualCursorChangedChecker(aDocAcc, aIdOrNameOrAcc, aTextOffsets) "wrong end offset"); } - var prevPosAndOffset = virtualCursorChangedChecker. + var prevPosAndOffset = VCChangedChecker. getPreviousPosAndOffset(aDocAcc.virtualCursor); if (prevPosAndOffset) { @@ -114,36 +116,36 @@ function virtualCursorChangedChecker(aDocAcc, aIdOrNameOrAcc, aTextOffsets) }; } -virtualCursorChangedChecker.prevPosAndOffset = {}; +VCChangedChecker.prevPosAndOffset = {}; -virtualCursorChangedChecker.storePreviousPosAndOffset = +VCChangedChecker.storePreviousPosAndOffset = function storePreviousPosAndOffset(aPivot) { - virtualCursorChangedChecker.prevPosAndOffset[aPivot] = + VCChangedChecker.prevPosAndOffset[aPivot] = {position: aPivot.position, startOffset: aPivot.startOffset, endOffset: aPivot.endOffset}; }; -virtualCursorChangedChecker.getPreviousPosAndOffset = +VCChangedChecker.getPreviousPosAndOffset = function getPreviousPosAndOffset(aPivot) { - return virtualCursorChangedChecker.prevPosAndOffset[aPivot]; + return VCChangedChecker.prevPosAndOffset[aPivot]; }; /** * Set a text range in the pivot and wait for virtual cursor change event. * - * @param aDocAcc document that manages the virtual cursor - * @param aTextAccessible accessible to set to virtual cursor's position - * @param aTextOffsets start and end offsets of text range to set in virtual - * cursor + * @param aDocAcc [in] document that manages the virtual cursor + * @param aTextAccessible [in] accessible to set to virtual cursor's position + * @param aTextOffsets [in] start and end offsets of text range to set in + * virtual cursor. */ -function setVirtualCursorRangeInvoker(aDocAcc, aTextAccessible, aTextOffsets) +function setVCRangeInvoker(aDocAcc, aTextAccessible, aTextOffsets) { this.invoke = function virtualCursorChangedInvoker_invoke() { - virtualCursorChangedChecker. + VCChangedChecker. storePreviousPosAndOffset(aDocAcc.virtualCursor); SimpleTest.info(prettyName(aTextAccessible) + " " + aTextOffsets); aDocAcc.virtualCursor.setTextRange(aTextAccessible, @@ -151,45 +153,44 @@ function setVirtualCursorRangeInvoker(aDocAcc, aTextAccessible, aTextOffsets) aTextOffsets[1]); }; - this.getID = function setVirtualCursorRangeInvoker_getID() + this.getID = function setVCRangeInvoker_getID() { return "Set offset in " + prettyName(aTextAccessible) + " to (" + aTextOffsets[0] + ", " + aTextOffsets[1] + ")"; - } + }; this.eventSeq = [ - new virtualCursorChangedChecker(aDocAcc, aTextAccessible, aTextOffsets) + new VCChangedChecker(aDocAcc, aTextAccessible, aTextOffsets) ]; } /** * Move the pivot and wait for virtual cursor change event. * - * @param aDocAcc document that manages the virtual cursor - * @param aPivotMoveMethod method to test (ie. "moveNext", "moveFirst", etc.) - * @param aRule traversal rule object - * @param aIdOrNameOrAcc id, accessivle or accessible name to expect virtual - * cursor to land on after performing move method. + * @param aDocAcc [in] document that manages the virtual cursor + * @param aPivotMoveMethod [in] method to test (ie. "moveNext", "moveFirst", etc.) + * @param aRule [in] traversal rule object + * @param aIdOrNameOrAcc [in] id, accessivle or accessible name to expect + * virtual cursor to land on after performing move method. */ -function setVirtualCursorPosInvoker(aDocAcc, aPivotMoveMethod, aRule, - aIdOrNameOrAcc) +function setVCPosInvoker(aDocAcc, aPivotMoveMethod, aRule, aIdOrNameOrAcc) { this.invoke = function virtualCursorChangedInvoker_invoke() { - virtualCursorChangedChecker. + VCChangedChecker. storePreviousPosAndOffset(aDocAcc.virtualCursor); var moved = aDocAcc.virtualCursor[aPivotMoveMethod](aRule); SimpleTest.ok((aIdOrNameOrAcc && moved) || (!aIdOrNameOrAcc && !moved), "moved pivot"); }; - this.getID = function setVirtualCursorPosInvoker_getID() + this.getID = function setVCPosInvoker_getID() { return "Do " + (aIdOrNameOrAcc ? "" : "no-op ") + aPivotMoveMethod; - } + }; if (aIdOrNameOrAcc) { - this.eventSeq = [ new virtualCursorChangedChecker(aDocAcc, aIdOrNameOrAcc) ]; + this.eventSeq = [ new VCChangedChecker(aDocAcc, aIdOrNameOrAcc) ]; } else { this.eventSeq = []; this.unexpectedEventSeq = [ @@ -202,45 +203,137 @@ function setVirtualCursorPosInvoker(aDocAcc, aPivotMoveMethod, aRule, * Add invokers to a queue to test a rule and an expected sequence of element ids * or accessible names for that rule in the given document. * - * @param aQueue event queue in which to push invoker sequence. - * @param aDocAcc the managing document of the virtual cursor we are testing - * @param aRule the traversal rule to use in the invokers - * @param aSequence a sequence of accessible names or elemnt ids to expect with - * the given rule in the given document + * @param aQueue [in] event queue in which to push invoker sequence. + * @param aDocAcc [in] the managing document of the virtual cursor we are testing + * @param aRule [in] the traversal rule to use in the invokers + * @param aSequence [in] a sequence of accessible names or elemnt ids to expect with + * the given rule in the given document */ function queueTraversalSequence(aQueue, aDocAcc, aRule, aSequence) { aDocAcc.virtualCursor.position = null; for (var i = 0; i < aSequence.length; i++) { - var invoker = new setVirtualCursorPosInvoker(aDocAcc, "moveNext", - aRule, aSequence[i]); + var invoker = + new setVCPosInvoker(aDocAcc, "moveNext", aRule, aSequence[i]); aQueue.push(invoker); } // No further more matches for given rule, expect no virtual cursor changes. - aQueue.push(new setVirtualCursorPosInvoker(aDocAcc, "moveNext", aRule, null)); + aQueue.push(new setVCPosInvoker(aDocAcc, "moveNext", aRule, null)); for (var i = aSequence.length-2; i >= 0; i--) { - var invoker = new setVirtualCursorPosInvoker(aDocAcc, "movePrevious", - aRule, aSequence[i]) + var invoker = + new setVCPosInvoker(aDocAcc, "movePrevious", aRule, aSequence[i]); aQueue.push(invoker); } // No previous more matches for given rule, expect no virtual cursor changes. - aQueue.push(new setVirtualCursorPosInvoker(aDocAcc, "movePrevious", aRule, null)); + aQueue.push(new setVCPosInvoker(aDocAcc, "movePrevious", aRule, null)); - aQueue.push(new setVirtualCursorPosInvoker( - aDocAcc, "moveLast", aRule, aSequence[aSequence.length - 1])); + aQueue.push(new setVCPosInvoker(aDocAcc, "moveLast", aRule, + aSequence[aSequence.length - 1])); // No further more matches for given rule, expect no virtual cursor changes. - aQueue.push(new setVirtualCursorPosInvoker(aDocAcc, "moveNext", aRule, null)); + aQueue.push(new setVCPosInvoker(aDocAcc, "moveNext", aRule, null)); - aQueue.push(new setVirtualCursorPosInvoker( - aDocAcc, "moveFirst", aRule, aSequence[0])); + aQueue.push(new setVCPosInvoker(aDocAcc, "moveFirst", aRule, aSequence[0])); // No previous more matches for given rule, expect no virtual cursor changes. - aQueue.push(new setVirtualCursorPosInvoker(aDocAcc, "movePrevious", aRule, null)); + aQueue.push(new setVCPosInvoker(aDocAcc, "movePrevious", aRule, null)); +} + +/** + * A checker for removing an accessible while the virtual cursor is on it. + */ +function removeVCPositionChecker(aDocAcc, aHiddenParentAcc) +{ + this.__proto__ = new invokerChecker(EVENT_REORDER, aHiddenParentAcc); + + this.check = function removeVCPositionChecker_check(aEvent) { + var errorResult = 0; + try { + aDocAcc.virtualCursor.moveNext(ObjectTraversalRule); + } catch (x) { + errorResult = x.result; + } + SimpleTest.is( + errorResult, NS_ERROR_NOT_IN_TREE, + "Expecting NOT_IN_TREE error when moving pivot from invalid position."); + }; +} + +/** + * Put the virtual cursor's position on an object, and then remove it. + * + * @param aDocAcc [in] document that manages the virtual cursor + * @param aPosNode [in] DOM node to hide after virtual cursor's position is + * set to it. + */ +function removeVCPositionInvoker(aDocAcc, aPosNode) +{ + this.accessible = getAccessible(aPosNode); + this.invoke = function removeVCPositionInvoker_invoke() + { + aDocAcc.virtualCursor.position = this.accessible; + aPosNode.parentNode.removeChild(aPosNode); + }; + + this.getID = function removeVCPositionInvoker_getID() + { + return "Bring virtual cursor to accessible, and remove its DOM node."; + }; + + this.eventSeq = [ + new removeVCPositionChecker(aDocAcc, this.accessible.parent) + ]; +} + +/** + * A checker for removing the pivot root and then calling moveFirst, and + * checking that an exception is thrown. + */ +function removeVCRootChecker(aPivot) +{ + this.__proto__ = new invokerChecker(EVENT_REORDER, aPivot.root.parent); + + this.check = function removeVCRootChecker_check(aEvent) { + var errorResult = 0; + try { + aPivot.moveLast(ObjectTraversalRule); + } catch (x) { + errorResult = x.result; + } + SimpleTest.is( + errorResult, NS_ERROR_NOT_IN_TREE, + "Expecting NOT_IN_TREE error when moving pivot from invalid position."); + }; +} + +/** + * Create a pivot, remove its root, and perform an operation where the root is + * needed. + * + * @param aRootNode [in] DOM node of which accessible will be the root of the + * pivot. Should have more than one child. + */ +function removeVCRootInvoker(aRootNode) +{ + this.pivot = gAccRetrieval.createAccessiblePivot(getAccessible(aRootNode)); + this.invoke = function removeVCRootInvoker_invoke() + { + this.pivot.position = this.pivot.root.firstChild; + aRootNode.parentNode.removeChild(aRootNode); + }; + + this.getID = function removeVCRootInvoker_getID() + { + return "Remove root of pivot from tree."; + }; + + this.eventSeq = [ + new removeVCRootChecker(this.pivot) + ]; } /** diff --git a/accessible/tests/mochitest/pivot/doc_virtualcursor.html b/accessible/tests/mochitest/pivot/doc_virtualcursor.html index 2ec42d1f60da..3c12b3d27c24 100644 --- a/accessible/tests/mochitest/pivot/doc_virtualcursor.html +++ b/accessible/tests/mochitest/pivot/doc_virtualcursor.html @@ -17,10 +17,11 @@ -
+