| /* |
| * Copyright (C) 2008 Nuanti Ltd. |
| * Copyright (C) 2009 Jan Alonzo |
| * Copyright (C) 2009, 2010, 2011, 2012 Igalia S.L. |
| * |
| * Portions from Mozilla a11y, copyright as follows: |
| * |
| * The Original Code is mozilla.org code. |
| * |
| * The Initial Developer of the Original Code is |
| * Sun Microsystems, Inc. |
| * Portions created by the Initial Developer are Copyright (C) 2002 |
| * the Initial Developer. 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 |
| * License as published by the Free Software Foundation; either |
| * version 2 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Library General Public License for more details. |
| * |
| * You should have received a copy of the GNU Library General Public License |
| * along with this library; see the file COPYING.LIB. If not, write to |
| * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
| * Boston, MA 02110-1301, USA. |
| */ |
| |
| #include "config.h" |
| #include "WebKitAccessibleInterfaceText.h" |
| |
| #include "AccessibilityObject.h" |
| #include "Document.h" |
| #include "Font.h" |
| #include "FrameView.h" |
| #include <wtf/gobject/GOwnPtr.h> |
| #include "HostWindow.h" |
| #include "InlineTextBox.h" |
| #include "NotImplemented.h" |
| #include "RenderListItem.h" |
| #include "RenderListMarker.h" |
| #include "RenderText.h" |
| #include "TextEncoding.h" |
| #include "TextIterator.h" |
| #include "WebKitAccessibleUtil.h" |
| #include "WebKitAccessibleWrapperAtk.h" |
| #include "htmlediting.h" |
| #include <libgail-util/gail-util.h> |
| #include <pango/pango.h> |
| |
| using namespace WebCore; |
| |
| static AccessibilityObject* core(AtkText* text) |
| { |
| if (!WEBKIT_IS_ACCESSIBLE(text)) |
| return 0; |
| |
| return webkitAccessibleGetAccessibilityObject(WEBKIT_ACCESSIBLE(text)); |
| } |
| |
| static gchar* textForRenderer(RenderObject* renderer) |
| { |
| GString* resultText = g_string_new(0); |
| |
| if (!renderer) |
| return g_string_free(resultText, FALSE); |
| |
| // For RenderBlocks, piece together the text from the RenderText objects they contain. |
| for (RenderObject* object = renderer->firstChild(); object; object = object->nextSibling()) { |
| if (object->isBR()) { |
| g_string_append(resultText, "\n"); |
| continue; |
| } |
| |
| RenderText* renderText; |
| if (object->isText()) |
| renderText = toRenderText(object); |
| else { |
| // List item's markers will be treated in an special way |
| // later on this function, so ignore them here. |
| if (object->isReplaced() && !object->isListMarker()) |
| g_string_append_unichar(resultText, objectReplacementCharacter); |
| |
| // We need to check children, if any, to consider when |
| // current object is not a text object but some of its |
| // children are, in order not to miss those portions of |
| // text by not properly handling those situations |
| if (object->firstChild()) |
| g_string_append(resultText, textForRenderer(object)); |
| |
| continue; |
| } |
| |
| InlineTextBox* box = renderText ? renderText->firstTextBox() : 0; |
| while (box) { |
| // WebCore introduces line breaks in the text that do not reflect |
| // the layout you see on the screen, replace them with spaces. |
| String text = String(renderText->characters(), renderText->textLength()).replace("\n", " "); |
| g_string_append(resultText, text.substring(box->start(), box->end() - box->start() + 1).utf8().data()); |
| |
| // Newline chars in the source result in separate text boxes, so check |
| // before adding a newline in the layout. See bug 25415 comment #78. |
| // If the next sibling is a BR, we'll add the newline when we examine that child. |
| if (!box->nextOnLineExists() && !(object->nextSibling() && object->nextSibling()->isBR())) { |
| // If there was a '\n' in the last position of the |
| // current text box, it would have been converted to a |
| // space in String::replace(), so remove it first. |
| if (renderText->characters()[box->end()] == '\n') |
| g_string_erase(resultText, resultText->len - 1, -1); |
| |
| g_string_append(resultText, "\n"); |
| } |
| box = box->nextTextBox(); |
| } |
| } |
| |
| // Insert the text of the marker for list item in the right place, if present |
| if (renderer->isListItem()) { |
| String markerText = toRenderListItem(renderer)->markerTextWithSuffix(); |
| if (renderer->style()->direction() == LTR) |
| g_string_prepend(resultText, markerText.utf8().data()); |
| else |
| g_string_append(resultText, markerText.utf8().data()); |
| } |
| |
| return g_string_free(resultText, FALSE); |
| } |
| |
| static gchar* textForObject(AccessibilityObject* coreObject) |
| { |
| GString* str = g_string_new(0); |
| |
| // For text controls, we can get the text line by line. |
| if (coreObject->isTextControl()) { |
| unsigned textLength = coreObject->textLength(); |
| int lineNumber = 0; |
| PlainTextRange range = coreObject->doAXRangeForLine(lineNumber); |
| while (range.length) { |
| // When a line of text wraps in a text area, the final space is removed. |
| if (range.start + range.length < textLength) |
| range.length -= 1; |
| String lineText = coreObject->doAXStringForRange(range); |
| g_string_append(str, lineText.utf8().data()); |
| g_string_append(str, "\n"); |
| range = coreObject->doAXRangeForLine(++lineNumber); |
| } |
| } else if (coreObject->isAccessibilityRenderObject()) { |
| GOwnPtr<gchar> rendererText(textForRenderer(coreObject->renderer())); |
| g_string_append(str, rendererText.get()); |
| } |
| |
| return g_string_free(str, FALSE); |
| } |
| |
| static gchar* webkitAccessibleTextGetText(AtkText*, gint startOffset, gint endOffset); |
| |
| static GailTextUtil* getGailTextUtilForAtk(AtkText* textObject) |
| { |
| GailTextUtil* gailTextUtil = gail_text_util_new(); |
| gail_text_util_text_setup(gailTextUtil, webkitAccessibleTextGetText(textObject, 0, -1)); |
| return gailTextUtil; |
| } |
| |
| static PangoLayout* getPangoLayoutForAtk(AtkText* textObject) |
| { |
| AccessibilityObject* coreObject = core(textObject); |
| |
| Document* document = coreObject->document(); |
| if (!document) |
| return 0; |
| |
| HostWindow* hostWindow = document->view()->hostWindow(); |
| if (!hostWindow) |
| return 0; |
| PlatformPageClient webView = hostWindow->platformPageClient(); |
| if (!webView) |
| return 0; |
| |
| // Create a string with the layout as it appears on the screen |
| PangoLayout* layout = gtk_widget_create_pango_layout(static_cast<GtkWidget*>(webView), textForObject(coreObject)); |
| return layout; |
| } |
| |
| static int baselinePositionForRenderObject(RenderObject* renderObject) |
| { |
| // FIXME: This implementation of baselinePosition originates from RenderObject.cpp and was |
| // removed in r70072. The implementation looks incorrect though, because this is not the |
| // baseline of the underlying RenderObject, but of the AccessibilityRenderObject. |
| const FontMetrics& fontMetrics = renderObject->firstLineStyle()->fontMetrics(); |
| return fontMetrics.ascent() + (renderObject->firstLineStyle()->computedLineHeight() - fontMetrics.height()) / 2; |
| } |
| |
| static AtkAttributeSet* getAttributeSetForAccessibilityObject(const AccessibilityObject* object) |
| { |
| if (!object->isAccessibilityRenderObject()) |
| return 0; |
| |
| RenderObject* renderer = object->renderer(); |
| RenderStyle* style = renderer->style(); |
| |
| AtkAttributeSet* result = 0; |
| GOwnPtr<gchar> buffer(g_strdup_printf("%i", style->fontSize())); |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_SIZE), buffer.get()); |
| |
| Color bgColor = style->visitedDependentColor(CSSPropertyBackgroundColor); |
| if (bgColor.isValid()) { |
| buffer.set(g_strdup_printf("%i,%i,%i", |
| bgColor.red(), bgColor.green(), bgColor.blue())); |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_BG_COLOR), buffer.get()); |
| } |
| |
| Color fgColor = style->visitedDependentColor(CSSPropertyColor); |
| if (fgColor.isValid()) { |
| buffer.set(g_strdup_printf("%i,%i,%i", |
| fgColor.red(), fgColor.green(), fgColor.blue())); |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_FG_COLOR), buffer.get()); |
| } |
| |
| int baselinePosition; |
| bool includeRise = true; |
| switch (style->verticalAlign()) { |
| case SUB: |
| baselinePosition = -1 * baselinePositionForRenderObject(renderer); |
| break; |
| case SUPER: |
| baselinePosition = baselinePositionForRenderObject(renderer); |
| break; |
| case BASELINE: |
| baselinePosition = 0; |
| break; |
| default: |
| includeRise = false; |
| break; |
| } |
| |
| if (includeRise) { |
| buffer.set(g_strdup_printf("%i", baselinePosition)); |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_RISE), buffer.get()); |
| } |
| |
| if (!style->textIndent().isUndefined()) { |
| int indentation = valueForLength(style->textIndent(), object->size().width(), renderer->view()); |
| buffer.set(g_strdup_printf("%i", indentation)); |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_INDENT), buffer.get()); |
| } |
| |
| String fontFamilyName = style->font().family().family().string(); |
| if (fontFamilyName.left(8) == "-webkit-") |
| fontFamilyName = fontFamilyName.substring(8); |
| |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_FAMILY_NAME), fontFamilyName.utf8().data()); |
| |
| int fontWeight = -1; |
| switch (style->font().weight()) { |
| case FontWeight100: |
| fontWeight = 100; |
| break; |
| case FontWeight200: |
| fontWeight = 200; |
| break; |
| case FontWeight300: |
| fontWeight = 300; |
| break; |
| case FontWeight400: |
| fontWeight = 400; |
| break; |
| case FontWeight500: |
| fontWeight = 500; |
| break; |
| case FontWeight600: |
| fontWeight = 600; |
| break; |
| case FontWeight700: |
| fontWeight = 700; |
| break; |
| case FontWeight800: |
| fontWeight = 800; |
| break; |
| case FontWeight900: |
| fontWeight = 900; |
| } |
| if (fontWeight > 0) { |
| buffer.set(g_strdup_printf("%i", fontWeight)); |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_WEIGHT), buffer.get()); |
| } |
| |
| switch (style->textAlign()) { |
| case TASTART: |
| case TAEND: |
| break; |
| case LEFT: |
| case WEBKIT_LEFT: |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_JUSTIFICATION), "left"); |
| break; |
| case RIGHT: |
| case WEBKIT_RIGHT: |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_JUSTIFICATION), "right"); |
| break; |
| case CENTER: |
| case WEBKIT_CENTER: |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_JUSTIFICATION), "center"); |
| break; |
| case JUSTIFY: |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_JUSTIFICATION), "fill"); |
| } |
| |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_UNDERLINE), (style->textDecoration() & UNDERLINE) ? "single" : "none"); |
| |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_STYLE), style->font().italic() ? "italic" : "normal"); |
| |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_STRIKETHROUGH), (style->textDecoration() & LINE_THROUGH) ? "true" : "false"); |
| |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_INVISIBLE), (style->visibility() == HIDDEN) ? "true" : "false"); |
| |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_EDITABLE), object->isReadOnly() ? "false" : "true"); |
| |
| return result; |
| } |
| |
| static gint compareAttribute(const AtkAttribute* a, const AtkAttribute* b) |
| { |
| return g_strcmp0(a->name, b->name) || g_strcmp0(a->value, b->value); |
| } |
| |
| // Returns an AtkAttributeSet with the elements of attributeSet1 which |
| // are either not present or different in attributeSet2. Neither |
| // attributeSet1 nor attributeSet2 should be used after calling this. |
| static AtkAttributeSet* attributeSetDifference(AtkAttributeSet* attributeSet1, AtkAttributeSet* attributeSet2) |
| { |
| if (!attributeSet2) |
| return attributeSet1; |
| |
| AtkAttributeSet* currentSet = attributeSet1; |
| AtkAttributeSet* found; |
| AtkAttributeSet* toDelete = 0; |
| |
| while (currentSet) { |
| found = g_slist_find_custom(attributeSet2, currentSet->data, (GCompareFunc)compareAttribute); |
| if (found) { |
| AtkAttributeSet* nextSet = currentSet->next; |
| toDelete = g_slist_prepend(toDelete, currentSet->data); |
| attributeSet1 = g_slist_delete_link(attributeSet1, currentSet); |
| currentSet = nextSet; |
| } else |
| currentSet = currentSet->next; |
| } |
| |
| atk_attribute_set_free(attributeSet2); |
| atk_attribute_set_free(toDelete); |
| return attributeSet1; |
| } |
| |
| static guint accessibilityObjectLength(const AccessibilityObject* object) |
| { |
| // Non render objects are not taken into account |
| if (!object->isAccessibilityRenderObject()) |
| return 0; |
| |
| // For those objects implementing the AtkText interface we use the |
| // well known API to always get the text in a consistent way |
| AtkObject* atkObj = ATK_OBJECT(object->wrapper()); |
| if (ATK_IS_TEXT(atkObj)) { |
| GOwnPtr<gchar> text(webkitAccessibleTextGetText(ATK_TEXT(atkObj), 0, -1)); |
| return g_utf8_strlen(text.get(), -1); |
| } |
| |
| // Even if we don't expose list markers to Assistive |
| // Technologies, we need to have a way to measure their length |
| // for those cases when it's needed to take it into account |
| // separately (as in getAccessibilityObjectForOffset) |
| RenderObject* renderer = object->renderer(); |
| if (renderer && renderer->isListMarker()) { |
| RenderListMarker* marker = toRenderListMarker(renderer); |
| return marker->text().length() + marker->suffix().length(); |
| } |
| |
| return 0; |
| } |
| |
| static const AccessibilityObject* getAccessibilityObjectForOffset(const AccessibilityObject* object, guint offset, gint* startOffset, gint* endOffset) |
| { |
| const AccessibilityObject* result; |
| guint length = accessibilityObjectLength(object); |
| if (length > offset) { |
| *startOffset = 0; |
| *endOffset = length; |
| result = object; |
| } else { |
| *startOffset = -1; |
| *endOffset = -1; |
| result = 0; |
| } |
| |
| if (!object->firstChild()) |
| return result; |
| |
| AccessibilityObject* child = object->firstChild(); |
| guint currentOffset = 0; |
| guint childPosition = 0; |
| while (child && currentOffset <= offset) { |
| guint childLength = accessibilityObjectLength(child); |
| currentOffset = childLength + childPosition; |
| if (currentOffset > offset) { |
| gint childStartOffset; |
| gint childEndOffset; |
| const AccessibilityObject* grandChild = getAccessibilityObjectForOffset(child, offset-childPosition, &childStartOffset, &childEndOffset); |
| if (childStartOffset >= 0) { |
| *startOffset = childStartOffset + childPosition; |
| *endOffset = childEndOffset + childPosition; |
| result = grandChild; |
| } |
| } else { |
| childPosition += childLength; |
| child = child->nextSibling(); |
| } |
| } |
| return result; |
| } |
| |
| static AtkAttributeSet* getRunAttributesFromAccesibilityObject(const AccessibilityObject* element, gint offset, gint* startOffset, gint* endOffset) |
| { |
| const AccessibilityObject* child = getAccessibilityObjectForOffset(element, offset, startOffset, endOffset); |
| if (!child) { |
| *startOffset = -1; |
| *endOffset = -1; |
| return 0; |
| } |
| |
| AtkAttributeSet* defaultAttributes = getAttributeSetForAccessibilityObject(element); |
| AtkAttributeSet* childAttributes = getAttributeSetForAccessibilityObject(child); |
| |
| return attributeSetDifference(childAttributes, defaultAttributes); |
| } |
| |
| static IntRect textExtents(AtkText* text, gint startOffset, gint length, AtkCoordType coords) |
| { |
| gchar* textContent = webkitAccessibleTextGetText(text, startOffset, -1); |
| gint textLength = g_utf8_strlen(textContent, -1); |
| |
| // The first case (endOffset of -1) should work, but seems broken for all Gtk+ apps. |
| gint rangeLength = length; |
| if (rangeLength < 0 || rangeLength > textLength) |
| rangeLength = textLength; |
| AccessibilityObject* coreObject = core(text); |
| |
| IntRect extents = coreObject->doAXBoundsForRange(PlainTextRange(startOffset, rangeLength)); |
| switch (coords) { |
| case ATK_XY_SCREEN: |
| if (Document* document = coreObject->document()) |
| extents = document->view()->contentsToScreen(extents); |
| break; |
| case ATK_XY_WINDOW: |
| // No-op |
| break; |
| } |
| |
| return extents; |
| } |
| |
| static void getSelectionOffsetsForObject(AccessibilityObject* coreObject, VisibleSelection& selection, gint& startOffset, gint& endOffset) |
| { |
| if (!coreObject->isAccessibilityRenderObject()) |
| return; |
| |
| // Early return if the selection doesn't affect the selected node. |
| if (!selectionBelongsToObject(coreObject, selection)) |
| return; |
| |
| // We need to find the exact start and end positions in the |
| // selected node that intersects the selection, to later on get |
| // the right values for the effective start and end offsets. |
| ExceptionCode ec = 0; |
| Position nodeRangeStart; |
| Position nodeRangeEnd; |
| Node* node = coreObject->node(); |
| RefPtr<Range> selRange = selection.toNormalizedRange(); |
| |
| // If the selection affects the selected node and its first |
| // possible position is also in the selection, we must set |
| // nodeRangeStart to that position, otherwise to the selection's |
| // start position (it would belong to the node anyway). |
| Node* firstLeafNode = node->firstDescendant(); |
| if (selRange->isPointInRange(firstLeafNode, 0, ec)) |
| nodeRangeStart = firstPositionInOrBeforeNode(firstLeafNode); |
| else |
| nodeRangeStart = selRange->startPosition(); |
| |
| // If the selection affects the selected node and its last |
| // possible position is also in the selection, we must set |
| // nodeRangeEnd to that position, otherwise to the selection's |
| // end position (it would belong to the node anyway). |
| Node* lastLeafNode = node->lastDescendant(); |
| if (selRange->isPointInRange(lastLeafNode, lastOffsetInNode(lastLeafNode), ec)) |
| nodeRangeEnd = lastPositionInOrAfterNode(lastLeafNode); |
| else |
| nodeRangeEnd = selRange->endPosition(); |
| |
| // Calculate position of the selected range inside the object. |
| Position parentFirstPosition = firstPositionInOrBeforeNode(node); |
| RefPtr<Range> rangeInParent = Range::create(node->document(), parentFirstPosition, nodeRangeStart); |
| |
| // Set values for start and end offsets. |
| startOffset = TextIterator::rangeLength(rangeInParent.get(), true); |
| |
| // We need to adjust the offsets for the list item marker. |
| RenderObject* renderer = coreObject->renderer(); |
| if (renderer && renderer->isListItem()) { |
| String markerText = toRenderListItem(renderer)->markerTextWithSuffix(); |
| startOffset += markerText.length(); |
| } |
| |
| RefPtr<Range> nodeRange = Range::create(node->document(), nodeRangeStart, nodeRangeEnd); |
| endOffset = startOffset + TextIterator::rangeLength(nodeRange.get(), true); |
| } |
| |
| static gchar* webkitAccessibleTextGetText(AtkText* text, gint startOffset, gint endOffset) |
| { |
| AccessibilityObject* coreObject = core(text); |
| |
| int end = endOffset; |
| if (endOffset == -1) { |
| end = coreObject->stringValue().length(); |
| if (!end) |
| end = coreObject->textUnderElement().length(); |
| } |
| |
| String ret; |
| if (coreObject->isTextControl()) |
| ret = coreObject->doAXStringForRange(PlainTextRange(0, endOffset)); |
| else { |
| ret = coreObject->stringValue(); |
| if (!ret) |
| ret = coreObject->textUnderElement(); |
| } |
| |
| if (!ret.length()) { |
| // This can happen at least with anonymous RenderBlocks (e.g. body text amongst paragraphs) |
| // In such instances, there may also be embedded objects. The object replacement character |
| // is something ATs want included and we have to account for the fact that it is multibyte. |
| ret = String::fromUTF8(textForObject(coreObject)); |
| if (!end) |
| end = ret.length(); |
| } |
| |
| // Prefix a item number/bullet if needed |
| if (coreObject->roleValue() == ListItemRole) { |
| RenderObject* objRenderer = coreObject->renderer(); |
| if (objRenderer && objRenderer->isListItem()) { |
| String markerText = toRenderListItem(objRenderer)->markerTextWithSuffix(); |
| ret = objRenderer->style()->direction() == LTR ? markerText + ret : ret + markerText; |
| if (endOffset == -1) |
| end += markerText.length(); |
| } |
| } |
| |
| ret = ret.substring(startOffset, end - startOffset); |
| return g_strdup(ret.utf8().data()); |
| } |
| |
| static gchar* webkitAccessibleTextGetTextAfterOffset(AtkText* text, gint offset, AtkTextBoundary boundaryType, gint* startOffset, gint* endOffset) |
| { |
| return gail_text_util_get_text(getGailTextUtilForAtk(text), getPangoLayoutForAtk(text), GAIL_AFTER_OFFSET, boundaryType, offset, startOffset, endOffset); |
| } |
| |
| static gchar* webkitAccessibleTextGetTextAtOffset(AtkText* text, gint offset, AtkTextBoundary boundaryType, gint* startOffset, gint* endOffset) |
| { |
| return gail_text_util_get_text(getGailTextUtilForAtk(text), getPangoLayoutForAtk(text), GAIL_AT_OFFSET, boundaryType, offset, startOffset, endOffset); |
| } |
| |
| static gchar* webkitAccessibleTextGetTextBeforeOffset(AtkText* text, gint offset, AtkTextBoundary boundaryType, gint* startOffset, gint* endOffset) |
| { |
| return gail_text_util_get_text(getGailTextUtilForAtk(text), getPangoLayoutForAtk(text), GAIL_BEFORE_OFFSET, boundaryType, offset, startOffset, endOffset); |
| } |
| |
| static gunichar webkitAccessibleTextGetCharacterAtOffset(AtkText*, gint) |
| { |
| notImplemented(); |
| return 0; |
| } |
| |
| static gint webkitAccessibleTextGetCaretOffset(AtkText* text) |
| { |
| // coreObject is the unignored object whose offset the caller is requesting. |
| // focusedObject is the object with the caret. It is likely ignored -- unless it's a link. |
| AccessibilityObject* coreObject = core(text); |
| if (!coreObject->isAccessibilityRenderObject()) |
| return 0; |
| |
| // We need to make sure we pass a valid object as reference. |
| if (coreObject->accessibilityIsIgnored()) |
| coreObject = coreObject->parentObjectUnignored(); |
| if (!coreObject) |
| return 0; |
| |
| int offset; |
| if (!objectFocusedAndCaretOffsetUnignored(coreObject, offset)) |
| return 0; |
| |
| RenderObject* renderer = coreObject->renderer(); |
| if (renderer && renderer->isListItem()) { |
| String markerText = toRenderListItem(renderer)->markerTextWithSuffix(); |
| |
| // We need to adjust the offset for the list item marker. |
| offset += markerText.length(); |
| } |
| |
| // TODO: Verify this for RTL text. |
| return offset; |
| } |
| |
| static AtkAttributeSet* webkitAccessibleTextGetRunAttributes(AtkText* text, gint offset, gint* startOffset, gint* endOffset) |
| { |
| AccessibilityObject* coreObject = core(text); |
| AtkAttributeSet* result; |
| |
| if (!coreObject) { |
| *startOffset = 0; |
| *endOffset = atk_text_get_character_count(text); |
| return 0; |
| } |
| |
| if (offset == -1) |
| offset = atk_text_get_caret_offset(text); |
| |
| result = getRunAttributesFromAccesibilityObject(coreObject, offset, startOffset, endOffset); |
| |
| if (*startOffset < 0) { |
| *startOffset = offset; |
| *endOffset = offset; |
| } |
| |
| return result; |
| } |
| |
| static AtkAttributeSet* webkitAccessibleTextGetDefaultAttributes(AtkText* text) |
| { |
| AccessibilityObject* coreObject = core(text); |
| if (!coreObject || !coreObject->isAccessibilityRenderObject()) |
| return 0; |
| |
| return getAttributeSetForAccessibilityObject(coreObject); |
| } |
| |
| static void webkitAccessibleTextGetCharacterExtents(AtkText* text, gint offset, gint* x, gint* y, gint* width, gint* height, AtkCoordType coords) |
| { |
| IntRect extents = textExtents(text, offset, 1, coords); |
| *x = extents.x(); |
| *y = extents.y(); |
| *width = extents.width(); |
| *height = extents.height(); |
| } |
| |
| static void webkitAccessibleTextGetRangeExtents(AtkText* text, gint startOffset, gint endOffset, AtkCoordType coords, AtkTextRectangle* rect) |
| { |
| IntRect extents = textExtents(text, startOffset, endOffset - startOffset, coords); |
| rect->x = extents.x(); |
| rect->y = extents.y(); |
| rect->width = extents.width(); |
| rect->height = extents.height(); |
| } |
| |
| static gint webkitAccessibleTextGetCharacterCount(AtkText* text) |
| { |
| return accessibilityObjectLength(core(text)); |
| } |
| |
| static gint webkitAccessibleTextGetOffsetAtPoint(AtkText* text, gint x, gint y, AtkCoordType coords) |
| { |
| // FIXME: Use the AtkCoordType |
| // TODO: Is it correct to ignore range.length? |
| IntPoint pos(x, y); |
| PlainTextRange range = core(text)->doAXRangeForPosition(pos); |
| return range.start; |
| } |
| |
| static gint webkitAccessibleTextGetNSelections(AtkText* text) |
| { |
| AccessibilityObject* coreObject = core(text); |
| VisibleSelection selection = coreObject->selection(); |
| |
| // Only range selections are needed for the purpose of this method |
| if (!selection.isRange()) |
| return 0; |
| |
| // We don't support multiple selections for now, so there's only |
| // two possibilities |
| // Also, we don't want to do anything if the selection does not |
| // belong to the currently selected object. We have to check since |
| // there's no way to get the selection for a given object, only |
| // the global one (the API is a bit confusing) |
| return selectionBelongsToObject(coreObject, selection) ? 1 : 0; |
| } |
| |
| 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; |
| |
| // Get the offsets of the selection for the selected object |
| AccessibilityObject* coreObject = core(text); |
| VisibleSelection selection = coreObject->selection(); |
| getSelectionOffsetsForObject(coreObject, selection, *startOffset, *endOffset); |
| |
| // Return 0 instead of "", as that's the expected result for |
| // this AtkText method when there's no selection |
| if (*startOffset == *endOffset) |
| return 0; |
| |
| return webkitAccessibleTextGetText(text, *startOffset, *endOffset); |
| } |
| |
| static gboolean webkitAccessibleTextAddSelection(AtkText*, gint, gint) |
| { |
| notImplemented(); |
| return FALSE; |
| } |
| |
| static gboolean webkitAccessibleTextSetSelection(AtkText* text, gint selectionNum, gint startOffset, gint endOffset) |
| { |
| // WebCore does not support multiple selection, so anything but 0 does not make sense for now. |
| if (selectionNum) |
| return FALSE; |
| |
| AccessibilityObject* coreObject = core(text); |
| if (!coreObject->isAccessibilityRenderObject()) |
| return FALSE; |
| |
| // Consider -1 and out-of-bound values and correct them to length |
| gint textCount = webkitAccessibleTextGetCharacterCount(text); |
| if (startOffset < 0 || startOffset > textCount) |
| startOffset = textCount; |
| if (endOffset < 0 || endOffset > textCount) |
| endOffset = textCount; |
| |
| // We need to adjust the offsets for the list item marker. |
| RenderObject* renderer = coreObject->renderer(); |
| if (renderer && renderer->isListItem()) { |
| String markerText = toRenderListItem(renderer)->markerTextWithSuffix(); |
| int markerLength = markerText.length(); |
| if (startOffset < markerLength || endOffset < markerLength) |
| return FALSE; |
| |
| startOffset -= markerLength; |
| endOffset -= markerLength; |
| } |
| |
| PlainTextRange textRange(startOffset, endOffset - startOffset); |
| VisiblePositionRange range = coreObject->visiblePositionRangeForRange(textRange); |
| if (range.isNull()) |
| return FALSE; |
| |
| coreObject->setSelectedVisiblePositionRange(range); |
| return TRUE; |
| } |
| |
| static gboolean webkitAccessibleTextRemoveSelection(AtkText* text, gint selectionNum) |
| { |
| // WebCore does not support multiple selection, so anything but 0 does not make sense for now. |
| if (selectionNum) |
| return FALSE; |
| |
| // Do nothing if current selection doesn't belong to the object |
| if (!webkitAccessibleTextGetNSelections(text)) |
| return FALSE; |
| |
| // Set a new 0-sized selection to the caret position, in order |
| // to simulate selection removal (GAIL style) |
| gint caretOffset = webkitAccessibleTextGetCaretOffset(text); |
| return webkitAccessibleTextSetSelection(text, selectionNum, caretOffset, caretOffset); |
| } |
| |
| static gboolean webkitAccessibleTextSetCaretOffset(AtkText* text, gint offset) |
| { |
| AccessibilityObject* coreObject = core(text); |
| |
| if (!coreObject->isAccessibilityRenderObject()) |
| return FALSE; |
| |
| RenderObject* renderer = coreObject->renderer(); |
| if (renderer && renderer->isListItem()) { |
| String markerText = toRenderListItem(renderer)->markerTextWithSuffix(); |
| int markerLength = markerText.length(); |
| if (offset < markerLength) |
| return FALSE; |
| |
| // We need to adjust the offset for list items. |
| offset -= markerLength; |
| } |
| |
| PlainTextRange textRange(offset, 0); |
| VisiblePositionRange range = coreObject->visiblePositionRangeForRange(textRange); |
| if (range.isNull()) |
| return FALSE; |
| |
| coreObject->setSelectedVisiblePositionRange(range); |
| return TRUE; |
| } |
| |
| void webkitAccessibleTextInterfaceInit(AtkTextIface* iface) |
| { |
| iface->get_text = webkitAccessibleTextGetText; |
| iface->get_text_after_offset = webkitAccessibleTextGetTextAfterOffset; |
| iface->get_text_at_offset = webkitAccessibleTextGetTextAtOffset; |
| iface->get_text_before_offset = webkitAccessibleTextGetTextBeforeOffset; |
| iface->get_character_at_offset = webkitAccessibleTextGetCharacterAtOffset; |
| iface->get_caret_offset = webkitAccessibleTextGetCaretOffset; |
| iface->get_run_attributes = webkitAccessibleTextGetRunAttributes; |
| iface->get_default_attributes = webkitAccessibleTextGetDefaultAttributes; |
| iface->get_character_extents = webkitAccessibleTextGetCharacterExtents; |
| iface->get_range_extents = webkitAccessibleTextGetRangeExtents; |
| iface->get_character_count = webkitAccessibleTextGetCharacterCount; |
| iface->get_offset_at_point = webkitAccessibleTextGetOffsetAtPoint; |
| iface->get_n_selections = webkitAccessibleTextGetNSelections; |
| iface->get_selection = webkitAccessibleTextGetSelection; |
| iface->add_selection = webkitAccessibleTextAddSelection; |
| iface->remove_selection = webkitAccessibleTextRemoveSelection; |
| iface->set_selection = webkitAccessibleTextSetSelection; |
| iface->set_caret_offset = webkitAccessibleTextSetCaretOffset; |
| } |