Modified: trunk/Source/WebCore/accessibility/atk/WebKitAccessibleInterfaceText.cpp (153651 => 153652)
--- trunk/Source/WebCore/accessibility/atk/WebKitAccessibleInterfaceText.cpp 2013-08-02 15:52:39 UTC (rev 153651)
+++ trunk/Source/WebCore/accessibility/atk/WebKitAccessibleInterfaceText.cpp 2013-08-02 16:02:11 UTC (rev 153652)
@@ -2,6 +2,7 @@
* Copyright (C) 2008 Nuanti Ltd.
* Copyright (C) 2009 Jan Alonzo
* Copyright (C) 2009, 2010, 2011, 2012 Igalia S.L.
+ * Copyright (C) 2013 Samsung Electronics. All rights reserved.
*
* Portions from Mozilla a11y, copyright as follows:
*
@@ -49,6 +50,7 @@
#include "WebKitAccessibleUtil.h"
#include "WebKitAccessibleWrapperAtk.h"
#include "htmlediting.h"
+#include <wtf/NotFound.h>
#include <wtf/gobject/GOwnPtr.h>
#include <wtf/text/CString.h>
@@ -136,7 +138,7 @@
return g_string_free(resultText, FALSE);
}
-static gchar* textForObject(AccessibilityObject* coreObject)
+static gchar* textForObject(const AccessibilityObject* coreObject)
{
GString* str = g_string_new(0);
@@ -559,6 +561,26 @@
startOffset = webCoreOffsetToAtkOffset(coreObject, TextIterator::rangeLength(rangeInParent.get(), true));
RefPtr<Range> nodeRange = Range::create(node->document(), nodeRangeStart, nodeRangeEnd);
int rangeLength = TextIterator::rangeLength(nodeRange.get(), true);
+
+ // Special cases that are only relevant when working with *_END boundaries.
+ if (selection.affinity() == UPSTREAM) {
+ VisiblePosition visibleStart(nodeRangeStart, UPSTREAM);
+ VisiblePosition visibleEnd(nodeRangeEnd, UPSTREAM);
+
+ // We need to adjust offsets when finding wrapped lines so the position at the end
+ // of the line is properly taking into account when calculating the offsets.
+ if (isEndOfLine(visibleStart) && !lineBreakExistsAtVisiblePosition(visibleStart)) {
+ if (isStartOfLine(visibleStart.next()))
+ rangeLength++;
+
+ if (!isEndOfBlock(visibleStart))
+ startOffset = std::max(startOffset - 1, 0);
+ }
+
+ if (isEndOfLine(visibleEnd) && !lineBreakExistsAtVisiblePosition(visibleEnd) && !isEndOfBlock(visibleEnd))
+ rangeLength--;
+ }
+
endOffset = std::min(startOffset + rangeLength, static_cast<int>(accessibilityObjectLength(coreObject)));
}
@@ -613,12 +635,16 @@
GetTextPositionAfter
};
-static gchar* webkitAccessibleTextGetChar(AtkText* text, gint offset, GetTextRelativePosition textPosition, gint* startOffset, gint* endOffset)
+// Convenience function to be used in early returns.
+static char* emptyTextSelectionAtOffset(int offset, int* startOffset, int* endOffset)
{
- AccessibilityObject* coreObject = core(text);
- if (!coreObject || !coreObject->isAccessibilityRenderObject())
- return g_strdup("");
+ *startOffset = offset;
+ *endOffset = offset;
+ return g_strdup("");
+}
+static char* webkitAccessibleTextGetChar(AtkText* text, int offset, GetTextRelativePosition textPosition, int* startOffset, int* endOffset)
+{
int actualOffset = offset;
if (textPosition == GetTextPositionBefore)
actualOffset--;
@@ -640,18 +666,187 @@
return g_utf8_substring(textData.get(), *startOffset, *endOffset);
}
+static VisiblePosition nextWordStartPosition(const VisiblePosition &position)
+{
+ VisiblePosition positionAfterCurrentWord = nextWordPosition(position);
+
+ // In order to skip spaces when moving right, we advance one word further
+ // and then move one word back. This will put us at the beginning of the
+ // word following.
+ VisiblePosition positionAfterSpacingAndFollowingWord = nextWordPosition(positionAfterCurrentWord);
+
+ if (positionAfterSpacingAndFollowingWord != positionAfterCurrentWord)
+ positionAfterCurrentWord = previousWordPosition(positionAfterSpacingAndFollowingWord);
+
+ bool movingBackwardsMovedPositionToStartOfCurrentWord = positionAfterCurrentWord == previousWordPosition(nextWordPosition(position));
+ if (movingBackwardsMovedPositionToStartOfCurrentWord)
+ positionAfterCurrentWord = positionAfterSpacingAndFollowingWord;
+
+ return positionAfterCurrentWord;
+}
+
+static VisiblePosition previousWordEndPosition(const VisiblePosition &position)
+{
+ // We move forward and then backward to position ourselves at the beginning
+ // of the current word for this boundary, making the most of the semantics
+ // of previousWordPosition() and nextWordPosition().
+ VisiblePosition positionAtStartOfCurrentWord = previousWordPosition(nextWordPosition(position));
+ VisiblePosition positionAtPreviousWord = previousWordPosition(position);
+
+ // Need to consider special cases (punctuation) when we are in the last word of a sentence.
+ if (isStartOfWord(position) && positionAtPreviousWord != position && positionAtPreviousWord == positionAtStartOfCurrentWord)
+ return nextWordPosition(positionAtStartOfCurrentWord);
+
+ // In order to skip spaces when moving left, we advance one word backwards
+ // and then move one word forward. This will put us at the beginning of
+ // the word following.
+ VisiblePosition positionBeforeSpacingAndPreceedingWord = previousWordPosition(positionAtStartOfCurrentWord);
+
+ if (positionBeforeSpacingAndPreceedingWord != positionAtStartOfCurrentWord)
+ positionAtStartOfCurrentWord = nextWordPosition(positionBeforeSpacingAndPreceedingWord);
+
+ bool movingForwardMovedPositionToEndOfCurrentWord = nextWordPosition(positionAtStartOfCurrentWord) == previousWordPosition(nextWordPosition(position));
+ if (movingForwardMovedPositionToEndOfCurrentWord)
+ positionAtStartOfCurrentWord = positionBeforeSpacingAndPreceedingWord;
+
+ return positionAtStartOfCurrentWord;
+}
+
+static VisibleSelection wordAtPositionForAtkBoundary(const AccessibilityObject* coreObject, const VisiblePosition& position, AtkTextBoundary boundaryType)
+{
+ VisiblePosition startPosition;
+ VisiblePosition endPosition;
+
+ switch (boundaryType) {
+ case ATK_TEXT_BOUNDARY_WORD_START:
+ // isStartOfWord() returns true both when at the beginning of a "real" word
+ // as when at the beginning of a whitespace range between two "real" words,
+ // since that whitespace is considered a "word" as well. And in case we are
+ // already at the beginning of a "real" word we do not need to look backwards.
+ if (isStartOfWord(position) && isWhitespace(position.characterBefore()))
+ startPosition = position;
+ else
+ startPosition = previousWordPosition(position);
+ endPosition = nextWordStartPosition(startPosition);
+
+ // We need to make sure that we look for the word in the current line when
+ // we are at the beginning of a new line, and not look into the previous one
+ // at all, which might happen when lines belong to different nodes.
+ if (isStartOfLine(position) && isStartOfLine(endPosition)) {
+ startPosition = endPosition;
+ endPosition = nextWordStartPosition(startPosition);
+ }
+ break;
+
+ case ATK_TEXT_BOUNDARY_WORD_END:
+ startPosition = previousWordEndPosition(position);
+ endPosition = nextWordPosition(startPosition);
+ break;
+
+ default:
+ ASSERT_NOT_REACHED();
+ }
+
+ VisibleSelection selectedWord(startPosition, endPosition);
+
+ // We mark the selection as 'upstream' so we can use that information later,
+ // when finding the actual offsets in getSelectionOffsetsForObject().
+ if (boundaryType == ATK_TEXT_BOUNDARY_WORD_END)
+ selectedWord.setAffinity(UPSTREAM);
+
+ return selectedWord;
+}
+
+static int numberOfReplacedElementsBeforeOffset(AtkText* text, unsigned offset)
+{
+ GOwnPtr<char> textForObject(webkitAccessibleTextGetText(text, 0, offset));
+ String textBeforeOffset = String::fromUTF8(textForObject.get());
+
+ int count = 0;
+ size_t index = textBeforeOffset.find(objectReplacementCharacter, 0);
+ while (index < offset && index != WTF::notFound) {
+ index = textBeforeOffset.find(objectReplacementCharacter, index + 1);
+ count++;
+ }
+ return count;
+}
+
+static char* webkitAccessibleTextGetWordForBoundary(AtkText* text, int offset, AtkTextBoundary boundaryType, GetTextRelativePosition textPosition, int* startOffset, int* endOffset)
+{
+ AccessibilityObject* coreObject = core(text);
+ Document* document = coreObject->document();
+ if (!document)
+ return emptyTextSelectionAtOffset(0, startOffset, endOffset);
+
+ Node* node = getNodeForAccessibilityObject(coreObject);
+ if (!node)
+ return emptyTextSelectionAtOffset(0, startOffset, endOffset);
+
+ int actualOffset = atkOffsetToWebCoreOffset(text, offset);
+
+ // Besides of the usual conversion from ATK offsets to WebCore offsets,
+ // we need to consider the potential embedded objects that might have been
+ // inserted in the text exposed through AtkText when calculating the offset.
+ actualOffset -= numberOfReplacedElementsBeforeOffset(text, actualOffset);
+
+ VisiblePosition caretPosition = coreObject->visiblePositionForIndex(actualOffset);
+ VisibleSelection currentWord = wordAtPositionForAtkBoundary(coreObject, caretPosition, boundaryType);
+
+ // Take into account other relative positions, if needed, by
+ // calculating the new position that we would need to consider.
+ VisiblePosition newPosition = caretPosition;
+ switch (textPosition) {
+ case GetTextPositionAt:
+ break;
+
+ case GetTextPositionBefore:
+ // Early return if asking for the previous word while already at the beginning.
+ if (isFirstVisiblePositionInNode(currentWord.visibleStart(), node))
+ return emptyTextSelectionAtOffset(0, startOffset, endOffset);
+
+ if (isStartOfLine(currentWord.end()))
+ newPosition = currentWord.visibleStart().previous();
+ else
+ newPosition = startOfWord(currentWord.start(), LeftWordIfOnBoundary);
+ break;
+
+ case GetTextPositionAfter:
+ // Early return if asking for the following word while already at the end.
+ if (isLastVisiblePositionInNode(currentWord.visibleEnd(), node))
+ return emptyTextSelectionAtOffset(accessibilityObjectLength(coreObject), startOffset, endOffset);
+
+ if (isEndOfLine(currentWord.end()))
+ newPosition = currentWord.visibleEnd().next();
+ else
+ newPosition = endOfWord(currentWord.end(), RightWordIfOnBoundary);
+ break;
+
+ default:
+ ASSERT_NOT_REACHED();
+ }
+
+ // Determine the relevant word we are actually interested in
+ // and calculate the ATK offsets for it, then return everything.
+ VisibleSelection selectedWord = newPosition != caretPosition ? wordAtPositionForAtkBoundary(coreObject, newPosition, boundaryType) : currentWord;
+ getSelectionOffsetsForObject(coreObject, selectedWord, *startOffset, *endOffset);
+ return webkitAccessibleTextGetText(text, *startOffset, *endOffset);
+}
+
static gchar* webkitAccessibleTextGetTextForOffset(AtkText* text, gint offset, AtkTextBoundary boundaryType, GetTextRelativePosition textPosition, gint* startOffset, gint* endOffset)
{
- // Make sure we always return valid valid values for offsets.
- *startOffset = 0;
- *endOffset = 0;
+ AccessibilityObject* coreObject = core(text);
+ if (!coreObject || !coreObject->isAccessibilityRenderObject())
+ return emptyTextSelectionAtOffset(0, startOffset, endOffset);
if (boundaryType == ATK_TEXT_BOUNDARY_CHAR)
return webkitAccessibleTextGetChar(text, offset, textPosition, startOffset, endOffset);
+ if (boundaryType == ATK_TEXT_BOUNDARY_WORD_START || boundaryType == ATK_TEXT_BOUNDARY_WORD_END)
+ return webkitAccessibleTextGetWordForBoundary(text, offset, boundaryType, textPosition, startOffset, endOffset);
+
#if PLATFORM(GTK)
- // FIXME: Get rid of the code below once every single get_text_*_offset
- // function has been properly implemented without using Pango/Cairo.
+ // FIXME: Get rid of the code below once every single part above
+ // has been properly implemented without using Pango/Cairo.
GailOffsetType offsetType = GAIL_AT_OFFSET;
switch (textPosition) {
case GetTextPositionBefore:
@@ -669,6 +864,10 @@
ASSERT_NOT_REACHED();
}
+ // Make sure we always return valid valid values for offsets.
+ *startOffset = 0;
+ *endOffset = 0;
+
return gail_text_util_get_text(getGailTextUtilForAtk(text), getPangoLayoutForAtk(text), offsetType, boundaryType, offset, startOffset, endOffset);
#endif
@@ -803,9 +1002,6 @@
static gchar* webkitAccessibleTextGetSelection(AtkText* text, gint selectionNum, gint* startOffset, gint* endOffset)
{
- // Default values, unless the contrary is proved
- *startOffset = *endOffset = 0;
-
// WebCore does not support multiple selection, so anything but 0 does not make sense for now.
if (selectionNum)
return 0;
Modified: trunk/Source/WebKit/gtk/tests/testatk.c (153651 => 153652)
--- trunk/Source/WebKit/gtk/tests/testatk.c 2013-08-02 15:52:39 UTC (rev 153651)
+++ trunk/Source/WebKit/gtk/tests/testatk.c 2013-08-02 16:02:11 UTC (rev 153652)
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2009 Igalia S.L.
+ * Copyright (C) 2013 Samsung Electronics. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
@@ -52,6 +53,8 @@
static const char* contentsWithWrappedLines = "<html><body><p style='max-width:150px;'>This is one line wrapped because of the maximum width of its container.</p><p>This is another line wrapped<br>because of one forced<br>line break in the middle.</body></html>";
+static const char* contentsWithEmbeddedObjects = "<html><body>This is one line containing two <img> embedded objects <img> in the middle.</body></html>";
+
static const char* comboBoxSelector = "<html><body><select><option selected value='foo'>foo</option><option value='bar'>bar</option></select></body></html>";
static const char* embeddedObjects = "<html><body><p>Choose: <input value='foo' type='checkbox'/>foo <input value='bar' type='checkbox'/>bar (pick one)</p><p>Choose: <select name='foo'><option>bar</option><option>baz</option></select> (pick one)</p><p><input name='foobarbutton' value='foobar' type='button'/></p></body></html>";
@@ -1054,6 +1057,75 @@
g_object_unref(webView);
}
+static void testWebkitAtkGetTextAtOffsetWithEmbeddedObjects()
+{
+ WebKitWebView* webView = WEBKIT_WEB_VIEW(webkit_web_view_new());
+ g_object_ref_sink(webView);
+ GtkAllocation allocation = { 0, 0, 800, 600 };
+ gtk_widget_size_allocate(GTK_WIDGET(webView), &allocation);
+ webkit_web_view_load_string(webView, contentsWithEmbeddedObjects, 0, 0, 0);
+
+ /* Enable caret browsing. */
+ WebKitWebSettings* settings = webkit_web_view_get_settings(webView);
+ g_object_set(settings, "enable-caret-browsing", TRUE, NULL);
+ webkit_web_view_set_settings(webView, settings);
+
+ /* Get to the inner AtkText object. */
+ AtkObject* object = getWebAreaObject(webView);
+ g_assert(object);
+
+ /* Check the paragraph with the text wrapped because of max-width. */
+ AtkText* paragraph = ATK_TEXT(atk_object_ref_accessible_child(object, 0));
+ g_assert(ATK_IS_TEXT(paragraph));
+
+ gchar* text = atk_text_get_text(paragraph, 0, -1);
+ g_assert_cmpstr(text, ==, "This is one line containing two \357\277\274 embedded objects \357\277\274 in the middle.");
+ g_free(text);
+
+ /* Check right before the first embedded object */
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_CHAR, 32, "\357\277\274", 32, 33);
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_WORD_START, 32, "two \357\277\274 ", 28, 34);
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_WORD_END, 32, " \357\277\274 embedded", 31, 42);
+
+ /* Check right after the first embedded object (and before the first word after it) */
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_CHAR, 33, " ", 33, 34);
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_WORD_START, 33, "two \357\277\274 ", 28, 34);
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_WORD_END, 33, " \357\277\274 embedded", 31, 42);
+
+ /* Check at the beginning of the first word between the two embedded objects */
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_CHAR, 34, "e", 34, 35);
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_WORD_START, 34, "embedded ", 34, 43);
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_WORD_END, 34, " \357\277\274 embedded", 31, 42);
+
+ /* Check at the end of the first word between the two embedded objects */
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_CHAR, 42, " ", 42, 43);
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_WORD_START, 42, "embedded ", 34, 43);
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_WORD_END, 42, " objects", 42, 50);
+
+ /* Check right before the second embedded object */
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_CHAR, 51, "\357\277\274", 51, 52);
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_WORD_START, 51, "objects \357\277\274 ", 43, 53);
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_WORD_END, 51, " \357\277\274 in", 50, 55);
+
+ /* Check right after the second embedded object (and before the first word after it) */
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_CHAR, 52, " ", 52, 53);
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_WORD_START, 52, "objects \357\277\274 ", 43, 53);
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_WORD_END, 52, " \357\277\274 in", 50, 55);
+
+ /* Check at the beginning of the first word after the two embedded objects */
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_CHAR, 53, "i", 53, 54);
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_WORD_START, 53, "in ", 53, 56);
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_WORD_END, 53, " \357\277\274 in", 50, 55);
+
+ /* Check at the end of the first word after the two embedded objects */
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_CHAR, 55, " ", 55, 56);
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_WORD_START, 55, "in ", 53, 56);
+ testGetTextFunction(paragraph, atk_text_get_text_at_offset, ATK_TEXT_BOUNDARY_WORD_END, 55, " the", 55, 59);
+
+ g_object_unref(paragraph);
+ g_object_unref(webView);
+}
+
static void testWebkitAtkGetTextInParagraphAndBodySimple()
{
WebKitWebView* webView = WEBKIT_WEB_VIEW(webkit_web_view_new());
@@ -2125,6 +2197,7 @@
g_test_add_func("/webkit/atk/getTextAtOffsetWithPreformattedText", testWebkitAtkGetTextAtOffsetWithPreformattedText);
g_test_add_func("/webkit/atk/getTextAtOffsetWithSpecialCharacters", testWebkitAtkGetTextAtOffsetWithSpecialCharacters);
g_test_add_func("/webkit/atk/getTextAtOffsetWithWrappedLines", testWebkitAtkGetTextAtOffsetWithWrappedLines);
+ g_test_add_func("/webkit/atk/getTextAtOffsetWithEmbeddedObjects", testWebkitAtkGetTextAtOffsetWithEmbeddedObjects);
g_test_add_func("/webkit/atk/getTextInParagraphAndBodySimple", testWebkitAtkGetTextInParagraphAndBodySimple);
g_test_add_func("/webkit/atk/getTextInParagraphAndBodyModerate", testWebkitAtkGetTextInParagraphAndBodyModerate);
g_test_add_func("/webkit/atk/getTextInTable", testWebkitAtkGetTextInTable);