summaryrefslogtreecommitdiffstats
path: root/WebKit
diff options
context:
space:
mode:
authorSvetoslav Ganov <svetoslavganov@google.com>2010-06-21 09:58:25 -0700
committerSvetoslav Ganov <svetoslavganov@google.com>2010-09-13 11:03:33 -0700
commit44c1d8d6b388379ab977dd12b5cf5d972186b4f6 (patch)
tree427e8e0198b9bb0cfc63aaaff43cb73e8951b8dc /WebKit
parent43df208144c6a77abc560712195027cbc13ce9de (diff)
downloadexternal_webkit-44c1d8d6b388379ab977dd12b5cf5d972186b4f6.zip
external_webkit-44c1d8d6b388379ab977dd12b5cf5d972186b4f6.tar.gz
external_webkit-44c1d8d6b388379ab977dd12b5cf5d972186b4f6.tar.bz2
Accessibility support for WebViews
Change-Id: Ia63a74b0de66d40acb08ec0ea6f39dc85adac9be
Diffstat (limited to 'WebKit')
-rw-r--r--WebKit/android/jni/WebViewCore.cpp389
-rw-r--r--WebKit/android/jni/WebViewCore.h36
2 files changed, 381 insertions, 44 deletions
diff --git a/WebKit/android/jni/WebViewCore.cpp b/WebKit/android/jni/WebViewCore.cpp
index e7026d7..1ae92d1 100644
--- a/WebKit/android/jni/WebViewCore.cpp
+++ b/WebKit/android/jni/WebViewCore.cpp
@@ -28,6 +28,7 @@
#include "config.h"
#include "WebViewCore.h"
+#include "AccessibilityObject.h"
#include "BaseLayerAndroid.h"
#include "CachedNode.h"
#include "CachedRoot.h"
@@ -43,6 +44,7 @@
#include "EditorClientAndroid.h"
#include "EventHandler.h"
#include "EventNames.h"
+#include "ExceptionCode.h"
#include "FocusController.h"
#include "Font.h"
#include "Frame.h"
@@ -106,6 +108,7 @@
#include "WebFrameView.h"
#include "WindowsKeyboardCodes.h"
#include "android_graphics.h"
+#include "markup.h"
#include <JNIHelp.h>
#include <JNIUtility.h>
@@ -429,6 +432,7 @@ void WebViewCore::reset(bool fromConstructor)
m_screenWidth = 0;
m_screenHeight = 0;
m_groupForVisitedLinks = NULL;
+ m_currentNodeDomNavigationAxis = 0;
}
static bool layoutIfNeededRecursive(WebCore::Frame* f)
@@ -1881,50 +1885,360 @@ void WebViewCore::setSelection(int start, int end)
}
}
-String WebViewCore::modifySelection(const String& alter, const String& direction, const String& granularity)
+String WebViewCore::modifySelection(const int direction, const int axis)
{
DOMSelection* selection = m_mainFrame->domWindow()->getSelection();
+ if (selection->rangeCount() > 1)
+ selection->removeAllRanges();
+ switch (axis) {
+ case AXIS_CHARACTER:
+ case AXIS_WORD:
+ case AXIS_SENTENCE:
+ return modifySelectionTextNavigationAxis(selection, direction, axis);
+ case AXIS_HEADING:
+ case AXIS_SIBLING:
+ case AXIS_PARENT_FIRST_CHILD:
+ case AXIS_DOCUMENT:
+ return modifySelectionDomNavigationAxis(selection, direction, axis);
+ default:
+ LOGE("Invalid navigation axis: %d", axis);
+ return String();
+ }
+}
- if (selection->rangeCount() == 0) {
- Document* document = m_mainFrame->document();
- HTMLElement* body = document->body();
- ExceptionCode ec;
-
- PassRefPtr<Range> rangeRef = document->createRange();
- rangeRef->setStart(PassRefPtr<Node>(body), 0, ec);
- if (ec) {
- LOGE("Error setting range start. Error code: %d", ec);
- return String();
+String WebViewCore::modifySelectionTextNavigationAxis(DOMSelection* selection, int direction, int axis)
+{
+ String directionString;
+ if (direction == DIRECTION_FORWARD)
+ directionString = "forward";
+ else if (direction == DIRECTION_BACKWARD)
+ directionString = "backward";
+ else {
+ LOGE("Invalid direction: %d", direction);
+ return String();
+ }
+ String axisString;
+ if (axis == AXIS_CHARACTER)
+ axisString = "character";
+ else if (axis == AXIS_WORD)
+ axisString = "word";
+ else // axis == AXIS_SENTENCE
+ axisString = "sentence";
+
+ // TODO: Add support of IFrames.
+ HTMLElement* body = m_mainFrame->document()->body();
+
+ Node* focusNode = 0;
+ if (m_currentNodeDomNavigationAxis
+ && CacheBuilder::validNode(m_mainFrame, m_mainFrame, m_currentNodeDomNavigationAxis)) {
+ focusNode = m_currentNodeDomNavigationAxis;
+ m_currentNodeDomNavigationAxis = 0;
+ do {
+ focusNode = (direction == DIRECTION_FORWARD) ?
+ focusNode->traverseNextNode(body) :
+ focusNode->traversePreviousNode(body);
+ } while (focusNode && focusNode->isTextNode());
+ } else
+ focusNode = (selection->focusNode()) ? selection->focusNode() : currentFocus();
+
+ Text* currentNode = 0;
+ if (!focusNode) {
+ // we have no selection so start from the body or its recursively last child
+ focusNode = (direction == DIRECTION_FORWARD) ? body : body->lastDescendant();
+ if (focusNode->isTextNode())
+ currentNode = static_cast<Text*>(focusNode);
+ else
+ currentNode = traverseNonEmptyNonWhitespaceTextNode(focusNode, body, direction);
+ if (!setSelection(selection, currentNode, direction))
+ return String();
+ } else if (focusNode->isElementNode()) {
+ // find a non-empty text node in the current direction
+ currentNode = traverseNonEmptyNonWhitespaceTextNode(focusNode, body, direction);
+ if (!setSelection(selection, currentNode, direction))
+ return String();
+ } else {
+ currentNode = static_cast<Text*>(focusNode);
+ if (direction == DIRECTION_FORWARD) {
+ // if end of non-whitespace text go to the next non-empty text node
+ int higherIndex = (selection->focusOffset() > selection->anchorOffset()) ?
+ selection->focusOffset() : selection->anchorOffset();
+ String suffix = currentNode->data().substring(higherIndex, currentNode->length());
+ if (suffix.isEmpty() || suffix.stripWhiteSpace().isEmpty()) {
+ currentNode = traverseNonEmptyNonWhitespaceTextNode(currentNode, body, direction);
+ if (!setSelection(selection, currentNode, direction))
+ return String();
+ } else {
+ ExceptionCode ec = 0;
+ selection->collapseToEnd(ec);
+ if (ec)
+ LOGE("Error while collapsing selection. Error code: %d", ec);
+ }
+ } else {
+ // if beginning of non-whitespace text go to the previous non-empty text node
+ int lowerIndex = (selection->focusOffset() > selection->anchorOffset()) ?
+ selection->anchorOffset() : selection->focusOffset();
+ String prefix = currentNode->data().substring(0, lowerIndex);
+ if (prefix.isEmpty() || prefix.stripWhiteSpace().isEmpty()) {
+ currentNode = traverseNonEmptyNonWhitespaceTextNode(currentNode, body, direction);
+ if (!setSelection(selection, currentNode, direction))
+ return String();
+ } else {
+ ExceptionCode ec = 0;
+ selection->collapseToStart(ec);
+ if (ec)
+ LOGE("Error while collapsing selection. Error code: %d", ec);
+ }
}
+ }
- rangeRef->setEnd(PassRefPtr<Node>(body), 0, ec);
- if (ec) {
- LOGE("Error setting range end. Error code: %d", ec);
- return String();
+ // extend the selection - loop as an insurance it does not get stuck
+ currentNode = static_cast<Text*>(selection->focusNode());
+ int focusOffset = selection->focusOffset();
+ while (true) {
+ selection->modify("extend", directionString, axisString);
+ if (selection->focusNode() != currentNode || selection->focusOffset() != focusOffset)
+ break;
+ currentNode = traverseNonEmptyNonWhitespaceTextNode(currentNode, body, direction);
+ focusOffset = (direction == DIRECTION_FORWARD) ? 0 : currentNode->data().length();
+ // setSelection returns false if currentNode is 0 => the loop always terminates
+ if (!setSelection(selection, currentNode, direction))
+ return String();
+ }
+
+ if (direction == DIRECTION_FORWARD) {
+ // enforce the anchor node is a text node
+ if (selection->anchorNode()->isElementNode()) {
+ if (!setSelection(selection, selection->focusNode(), selection->focusNode(), 0,
+ selection->focusOffset()))
+ return String();
+ }
+ // enforce the focus node is a text node
+ if (selection->focusNode()->isElementNode()) {
+ int endOffset = static_cast<Text*>(selection->anchorNode())->length(); // cast is safe
+ if (!setSelection(selection, selection->anchorNode(), selection->anchorNode(),
+ selection->anchorOffset(), endOffset))
+ return String();
+ }
+ } else {
+ // enforce the focus node is a text node
+ if (selection->focusNode()->isElementNode()) {
+ if (!setSelection(selection, selection->anchorNode(), selection->anchorNode(), 0,
+ selection->anchorOffset()))
+ return String();
+ }
+ // enforce the anchor node is a text node
+ if (selection->anchorNode()->isElementNode()) {
+ int endOffset = static_cast<Text*>(selection->focusNode())->length(); // cast is safe
+ if (!setSelection(selection, selection->focusNode(), selection->focusNode(),
+ selection->focusOffset(), endOffset))
+ return String();
}
-
- selection->addRange(rangeRef.get());
}
- ExceptionCode ec;
- if (equalIgnoringCase(direction, "forward")) {
- selection->collapseToEnd(ec);
- } else if (equalIgnoringCase(direction, "backward")) {
- selection->collapseToStart(ec);
+ tryFocusInlineSelectionElement(selection);
+ // TODO (svetoslavganov): Draw the selected text in the WebView - a-la-Android
+ String markup = formatMarkup(selection).stripWhiteSpace();
+ LOGD("Selection markup: %s", markup.utf8().data());
+ return markup;
+}
+
+bool WebViewCore::setSelection(DOMSelection* selection, Text* textNode, int direction)
+{
+ if (!textNode)
+ return false;
+ int offset = (direction == DIRECTION_FORWARD) ? 0 : textNode->length();
+ if (!setSelection(selection, textNode, textNode, offset, offset))
+ return false;
+ return true;
+}
+
+bool WebViewCore::setSelection(DOMSelection* selection, Node* startNode, Node* endNode, int startOffset, int endOffset)
+{
+ if (!selection || (!startNode && !endNode))
+ return false;
+ ExceptionCode ec = 0;
+ PassRefPtr<Range> rangeRef = selection->getRangeAt(0, ec);
+ if (ec) {
+ ec = 0;
+ rangeRef = m_mainFrame->document()->createRange();
+ }
+ if (startNode)
+ rangeRef->setStart(PassRefPtr<Node>(startNode), startOffset, ec);
+ if (ec)
+ return false;
+ if (endNode)
+ rangeRef->setEnd(PassRefPtr<Node>(endNode), endOffset, ec);
+ if (ec)
+ return false;
+ selection->removeAllRanges();
+ selection->addRange(rangeRef.get());
+ return true;
+}
+
+Text* WebViewCore::traverseNonEmptyNonWhitespaceTextNode(Node* fromNode, Node* toNode, int direction)
+{
+ Node* currentNode = fromNode;
+ do {
+ if (direction == DIRECTION_FORWARD)
+ currentNode = currentNode->traverseNextNode(toNode);
+ else
+ currentNode = currentNode->traversePreviousNode(toNode);
+ } while (currentNode && (!currentNode->isTextNode()
+ || isEmptyOrOnlyWhitespaceTextNode(currentNode)));
+ return static_cast<Text*>(currentNode);
+}
+
+bool WebViewCore::isEmptyOrOnlyWhitespaceTextNode(Node* node)
+{
+ return (node->isTextNode()
+ && (static_cast<Text*>(node)->length() == 0
+ || static_cast<Text*>(node)->containsOnlyWhitespace()));
+}
+
+String WebViewCore::modifySelectionDomNavigationAxis(DOMSelection* selection, int direction, int axis)
+{
+ // TODO: Add support of IFrames.
+ HTMLElement* body = m_mainFrame->document()->body();
+ if (!m_currentNodeDomNavigationAxis && selection->focusNode()) {
+ m_currentNodeDomNavigationAxis = selection->focusNode();
+ selection->empty();
+ if (m_currentNodeDomNavigationAxis->isTextNode())
+ m_currentNodeDomNavigationAxis = m_currentNodeDomNavigationAxis->parentNode();
+ }
+ if (!m_currentNodeDomNavigationAxis)
+ m_currentNodeDomNavigationAxis = currentFocus();
+ if (!m_currentNodeDomNavigationAxis
+ || !CacheBuilder::validNode(m_mainFrame, m_mainFrame, m_currentNodeDomNavigationAxis))
+ m_currentNodeDomNavigationAxis = body;
+ Node* currentNode = m_currentNodeDomNavigationAxis;
+ if (axis == AXIS_HEADING) {
+ if (currentNode == body && direction == DIRECTION_BACKWARD)
+ currentNode = currentNode->lastDescendant();
+ do {
+ if (direction == DIRECTION_FORWARD)
+ currentNode = currentNode->traverseNextNode(body);
+ else
+ currentNode = currentNode->traversePreviousNode(body);
+ } while (currentNode && (currentNode->isTextNode() || !isVisible(currentNode)
+ || !isHeading(currentNode)));
+ } else if (axis == AXIS_PARENT_FIRST_CHILD) {
+ if (direction == DIRECTION_FORWARD) {
+ currentNode = currentNode->firstChild();
+ while (currentNode && (currentNode->isTextNode() || !isVisible(currentNode)))
+ currentNode = currentNode->nextSibling();
+ } else {
+ do {
+ if (currentNode == body)
+ return String();
+ currentNode = currentNode->parentNode();
+ } while (currentNode && (currentNode->isTextNode() || !isVisible(currentNode)));
+ }
+ } else if (axis == AXIS_SIBLING) {
+ do {
+ if (direction == DIRECTION_FORWARD)
+ currentNode = currentNode->nextSibling();
+ else {
+ if (currentNode == body)
+ return String();
+ currentNode = currentNode->previousSibling();
+ }
+ } while (currentNode && (currentNode->isTextNode() || !isVisible(currentNode)));
+ } else if (axis == AXIS_DOCUMENT) {
+ currentNode = body;
+ if (direction == DIRECTION_FORWARD)
+ currentNode = currentNode->lastDescendant();
} else {
- LOGE("Invalid direction: %s", direction.utf8().data());
+ LOGE("Invalid axis: %d", axis);
return String();
}
+ if (currentNode) {
+ m_currentNodeDomNavigationAxis = currentNode;
+ focusIfFocusableAndNotTextInput(selection, currentNode);
+ // TODO (svetoslavganov): Draw the selected text in the WebView - a-la-Android
+ String selectionString = createMarkup(currentNode);
+ LOGD("Selection markup: %s", selectionString.utf8().data());
+ return selectionString;
+ }
+ return String();
+}
+
+bool WebViewCore::isHeading(Node* node)
+{
+ if (node->hasTagName(WebCore::HTMLNames::h1Tag)
+ || node->hasTagName(WebCore::HTMLNames::h2Tag)
+ || node->hasTagName(WebCore::HTMLNames::h3Tag)
+ || node->hasTagName(WebCore::HTMLNames::h4Tag)
+ || node->hasTagName(WebCore::HTMLNames::h5Tag)
+ || node->hasTagName(WebCore::HTMLNames::h6Tag)) {
+ return true;
+ }
+
+ if (node->isElementNode()) {
+ Element* element = static_cast<Element*>(node);
+ String roleAttribute = element->getAttribute(WebCore::HTMLNames::roleAttr).string();
+ if (equalIgnoringCase(roleAttribute, "heading"))
+ return true;
+ }
+
+ return false;
+}
+
+bool WebViewCore::isVisible(Node* node)
+{
+ if (!node->isStyledElement())
+ return false;
+ RenderStyle* style = node->computedStyle();
+ return (style->display() != NONE && style->visibility() != HIDDEN);
+}
- // NOTE: The selection of WebKit misbehaves and I need to add some
- // voodoo here to force it behave well. Rachel did something similar
- // in JS and I want to make sure it is optimal before adding it here.
+String WebViewCore::formatMarkup(DOMSelection* selection)
+{
+ ExceptionCode ec = 0;
+ PassRefPtr<Range> rangeRef = selection->getRangeAt(0, ec);
+ if (ec) {
+ LOGE("Error accessing the first selection range. Error code: %d", ec);
+ return String();
+ }
+ // TODO: This breaks in certain cases - WebKit bug. Figure out and work around it
+ String markup = createMarkup(rangeRef.get());
+ int fromIdx = markup.find("<span class=\"Apple-style-span\"");
+ while (fromIdx > -1) {
+ unsigned toIdx = markup.find(">");
+ markup = markup.replace(fromIdx, toIdx - fromIdx + 1, "");
+ markup = markup.replace("</span>", "");
+ fromIdx = markup.find("<span class=\"Apple-style-span\"");
+ }
+ return markup;
+}
- selection->modify(alter, direction, granularity);
- String selection_string = selection->toString();
- LOGD("Selection string: %s", selection_string.utf8().data());
+void WebViewCore::tryFocusInlineSelectionElement(DOMSelection* selection)
+{
+ Node* currentNode = selection->anchorNode();
+ Node* endNode = selection->focusNode();
+ while (currentNode) {
+ if (focusIfFocusableAndNotTextInput(selection, currentNode))
+ return;
+ currentNode = currentNode->traverseNextNode(endNode);
+ }
+}
- return selection_string;
+bool WebViewCore::focusIfFocusableAndNotTextInput(DOMSelection* selection, Node* node)
+{
+ // TODO (svetoslavganov): Synchronize Android and WebKit focus
+ if (node->isFocusable()) {
+ WebCore::RenderObject* renderer = node->renderer();
+ if (!renderer || (!renderer->isTextField() && !renderer->isTextArea())) {
+ // restore the selection after focus workaround for
+ // the FIXME in Element.cpp#updateFocusAppearance
+ ExceptionCode ec = 0;
+ PassRefPtr<Range> rangeRef = selection->getRangeAt(0, ec);
+ moveFocus(m_mainFrame, node);
+ if (rangeRef)
+ selection->addRange(rangeRef.get());
+ return true;
+ }
+ }
+ return false;
}
void WebViewCore::deleteSelection(int start, int end, int textGeneration)
@@ -3030,21 +3344,14 @@ static void SetSelection(JNIEnv *env, jobject obj, jint start, jint end)
viewImpl->setSelection(start, end);
}
-static jstring ModifySelection(JNIEnv *env, jobject obj, jstring alter, jstring direction, jstring granularity)
+static jstring ModifySelection(JNIEnv *env, jobject obj, jint direction, jint granularity)
{
#ifdef ANDROID_INSTRUMENT
TimeCounterAuto counter(TimeCounter::WebViewCoreTimeCounter);
#endif
- String alterString = to_string(env, alter);
- String directionString = to_string(env, direction);
- String granularityString = to_string(env, granularity);
-
WebViewCore* viewImpl = GET_NATIVE_VIEW(env, obj);
- String selection_string = viewImpl->modifySelection(alterString,
- directionString,
- granularityString);
-
- return WebCoreStringToJString(env, selection_string);
+ String selectionString = viewImpl->modifySelection(direction, granularity);
+ return WebCoreStringToJString(env, selectionString);
}
static void ReplaceTextfieldText(JNIEnv *env, jobject obj,
@@ -3585,7 +3892,7 @@ static JNINativeMethod gJavaWebViewCoreMethods[] = {
(void*) SetGlobalBounds },
{ "nativeSetSelection", "(II)V",
(void*) SetSelection } ,
- { "nativeModifySelection", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",
+ { "nativeModifySelection", "(II)Ljava/lang/String;",
(void*) ModifySelection },
{ "nativeDeleteSelection", "(III)V",
(void*) DeleteSelection } ,
diff --git a/WebKit/android/jni/WebViewCore.h b/WebKit/android/jni/WebViewCore.h
index 176e1bf..00054f3 100644
--- a/WebKit/android/jni/WebViewCore.h
+++ b/WebKit/android/jni/WebViewCore.h
@@ -31,6 +31,7 @@
#include "CacheBuilder.h"
#include "CachedHistory.h"
#include "DeviceOrientationManager.h"
+#include "DOMSelection.h"
#include "PictureSet.h"
#include "PlatformGraphicsContext.h"
#include "SkColor.h"
@@ -74,6 +75,21 @@ class SkIRect;
namespace android {
+ enum Direction {
+ DIRECTION_BACKWARD = 0,
+ DIRECTION_FORWARD = 1
+ };
+
+ enum NavigationAxis {
+ AXIS_CHARACTER = 0,
+ AXIS_WORD = 1,
+ AXIS_SENTENCE = 2,
+ AXIS_HEADING = 3,
+ AXIS_SIBLING = 4,
+ AXIS_PARENT_FIRST_CHILD = 5,
+ AXIS_DOCUMENT = 6
+ };
+
class CachedFrame;
class CachedNode;
class CachedRoot;
@@ -330,13 +346,14 @@ namespace android {
/**
* Modifies the current selection.
*
- * alter - Specifies how to alter the selection.
* direction - The direction in which to alter the selection.
* granularity - The granularity of the selection modification.
*
- * returns - The selection as string.
+ * returns - The selected HTML as a string. This is not a well formed
+ * HTML, rather the selection annotated with the tags of all
+ * intermediary elements it crosses.
*/
- String modifySelection(const String& alter, const String& direction, const String& granularity);
+ String modifySelection(const int direction, const int granularity);
/**
* In the currently focused textfield, replace the characters from oldStart to oldEnd
@@ -568,6 +585,19 @@ namespace android {
void sendNotifyProgressFinished();
bool handleMouseClick(WebCore::Frame* framePtr, WebCore::Node* nodePtr);
WebCore::HTMLAnchorElement* retrieveAnchorElement(WebCore::Frame* frame, WebCore::Node* node);
+ // below are members responsible for accessibility support
+ String modifySelectionTextNavigationAxis(DOMSelection* selection, int direction, int granularity);
+ String modifySelectionDomNavigationAxis(DOMSelection* selection, int direction, int granularity);
+ Text* traverseNonEmptyNonWhitespaceTextNode(Node* fromNode, Node* toNode ,int direction);
+ bool isVisible(Node* node);
+ bool isHeading(Node* node);
+ bool isEmptyOrOnlyWhitespaceTextNode(Node* node);
+ String formatMarkup(DOMSelection* selection);
+ void tryFocusInlineSelectionElement(DOMSelection* selection);
+ bool focusIfFocusableAndNotTextInput(DOMSelection* selection, Node* node);
+ bool setSelection(DOMSelection* selection, Text* textNode, int direction);
+ bool setSelection(DOMSelection* selection, Node* startNode, Node* endNode, int startOffset, int endOffset);
+ Node* m_currentNodeDomNavigationAxis;
#if ENABLE(TOUCH_EVENTS)
bool m_forwardingTouchEvents;