| /* |
| * 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: |
| * |
| * 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" |
| |
| #if ENABLE(ACCESSIBILITY) && USE(ATK) |
| |
| #include "AccessibilityObject.h" |
| #include "Document.h" |
| #include "Editing.h" |
| #include "FontCascade.h" |
| #include "FrameView.h" |
| #include "HTMLParserIdioms.h" |
| #include "HostWindow.h" |
| #include "NotImplemented.h" |
| #include "Range.h" |
| #include "RenderListItem.h" |
| #include "RenderListMarker.h" |
| #include "RenderText.h" |
| #include "TextEncoding.h" |
| #include "TextIterator.h" |
| #include "VisibleUnits.h" |
| #include "WebKitAccessible.h" |
| #include "WebKitAccessibleUtil.h" |
| #include <wtf/glib/GUniquePtr.h> |
| #include <wtf/text/CString.h> |
| |
| using namespace WebCore; |
| |
| // Text attribute to expose the ARIA 'aria-invalid' attribute. Initially initialized |
| // to ATK_TEXT_ATTR_INVALID (which means 'invalid' text attribute'), will later on |
| // hold a reference to the custom registered AtkTextAttribute that we will use. |
| static AtkTextAttribute atkTextAttributeInvalid = ATK_TEXT_ATTR_INVALID; |
| |
| static AccessibilityObject* core(AtkText* text) |
| { |
| if (!WEBKIT_IS_ACCESSIBLE(text)) |
| return 0; |
| |
| return &webkitAccessibleGetAccessibilityObject(WEBKIT_ACCESSIBLE(text)); |
| } |
| |
| 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(); |
| auto* style = &renderer->style(); |
| |
| AtkAttributeSet* result = nullptr; |
| GUniquePtr<gchar> buffer(g_strdup_printf("%i", style->computedFontPixelSize())); |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_SIZE), buffer.get()); |
| |
| Color bgColor = style->visitedDependentColor(CSSPropertyBackgroundColor); |
| if (bgColor.isValid()) { |
| auto [r, g, b, a] = bgColor.toSRGBALossy<uint8_t>(); |
| buffer.reset(g_strdup_printf("%i,%i,%i", r, g, b)); |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_BG_COLOR), buffer.get()); |
| } |
| |
| Color fgColor = style->visitedDependentColor(CSSPropertyColor); |
| if (fgColor.isValid()) { |
| auto [r, g, b, a] = fgColor.toSRGBALossy<uint8_t>(); |
| buffer.reset(g_strdup_printf("%i,%i,%i", r, g, b)); |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_FG_COLOR), buffer.get()); |
| } |
| |
| int baselinePosition; |
| bool includeRise = true; |
| switch (style->verticalAlign()) { |
| case VerticalAlign::Sub: |
| baselinePosition = -1 * baselinePositionForRenderObject(renderer); |
| break; |
| case VerticalAlign::Super: |
| baselinePosition = baselinePositionForRenderObject(renderer); |
| break; |
| case VerticalAlign::Baseline: |
| baselinePosition = 0; |
| break; |
| default: |
| includeRise = false; |
| break; |
| } |
| |
| if (includeRise) { |
| buffer.reset(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()); |
| buffer.reset(g_strdup_printf("%i", indentation)); |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_INDENT), buffer.get()); |
| } |
| |
| String fontFamilyName = style->fontCascade().firstFamily(); |
| 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 = static_cast<float>(style->fontCascade().weight()); |
| if (fontWeight > 0) { |
| buffer.reset(g_strdup_printf("%i", fontWeight)); |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_WEIGHT), buffer.get()); |
| } |
| |
| switch (style->textAlign()) { |
| case TextAlignMode::Start: |
| case TextAlignMode::End: |
| break; |
| case TextAlignMode::Left: |
| case TextAlignMode::WebKitLeft: |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_JUSTIFICATION), "left"); |
| break; |
| case TextAlignMode::Right: |
| case TextAlignMode::WebKitRight: |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_JUSTIFICATION), "right"); |
| break; |
| case TextAlignMode::Center: |
| case TextAlignMode::WebKitCenter: |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_JUSTIFICATION), "center"); |
| break; |
| case TextAlignMode::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() & TextDecorationLine::Underline) ? "single" : "none"); |
| |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_STYLE), style->fontCascade().italic() ? "italic" : "normal"); |
| |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_STRIKETHROUGH), (style->textDecoration() & TextDecorationLine::LineThrough) ? "true" : "false"); |
| |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_INVISIBLE), (style->visibility() == Visibility::Hidden) ? "true" : "false"); |
| |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_EDITABLE), object->canSetValueAttribute() ? "true" : "false"); |
| |
| String language = object->language(); |
| if (!language.isEmpty()) |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(ATK_TEXT_ATTR_LANGUAGE), language.utf8().data()); |
| |
| String invalidStatus = object->invalidStatus(); |
| if (invalidStatus != "false") { |
| // Register the custom attribute for 'aria-invalid' if not done yet. |
| if (atkTextAttributeInvalid == ATK_TEXT_ATTR_INVALID) |
| atkTextAttributeInvalid = atk_text_attribute_register("invalid"); |
| |
| result = addToAtkAttributeSet(result, atk_text_attribute_get_name(atkTextAttributeInvalid), invalidStatus.utf8().data()); |
| } |
| |
| 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 = nullptr; |
| |
| 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 gchar* webkitAccessibleTextGetText(AtkText*, gint startOffset, gint endOffset); |
| |
| 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 |
| auto* atkObj = ATK_OBJECT(object->wrapper()); |
| if (ATK_IS_TEXT(atkObj)) { |
| GUniquePtr<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) |
| if (auto renderer = object->renderer(); is<RenderListMarker>(renderer)) |
| return downcast<RenderListMarker>(*renderer).textWithSuffix().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* getRunAttributesFromAccessibilityObject(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) |
| { |
| GUniquePtr<char> textContent(webkitAccessibleTextGetText(text, startOffset, -1)); |
| gint textLength = g_utf8_strlen(textContent.get(), -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; |
| #if ATK_CHECK_VERSION(2, 30, 0) |
| case ATK_XY_PARENT: |
| RELEASE_ASSERT_NOT_REACHED(); |
| #endif |
| } |
| |
| return extents; |
| } |
| |
| static int offsetAdjustmentForListItem(const AXCoreObject* object) |
| { |
| // We need to adjust the offsets for the list item marker in |
| // Left-To-Right text, since we expose it together with the text. |
| RenderObject* renderer = object->renderer(); |
| if (is<RenderListItem>(renderer) && renderer->style().direction() == TextDirection::LTR) |
| return downcast<RenderListItem>(*renderer).markerTextWithSuffix().length(); |
| |
| return 0; |
| } |
| |
| static int webCoreOffsetToAtkOffset(const AXCoreObject* object, int offset) |
| { |
| if (!object->isListItem()) |
| return offset; |
| |
| return offset + offsetAdjustmentForListItem(object); |
| } |
| |
| static int atkOffsetToWebCoreOffset(AtkText* text, int offset) |
| { |
| AccessibilityObject* coreObject = core(text); |
| if (!coreObject || !coreObject->isListItem()) |
| return offset; |
| |
| return offset - offsetAdjustmentForListItem(coreObject); |
| } |
| |
| static Node* getNodeForAccessibilityObject(AccessibilityObject* coreObject) |
| { |
| if (!coreObject->isNativeTextControl()) |
| return coreObject->node(); |
| |
| // For text controls, we get the first visible position on it (which will |
| // belong to its inner element, unreachable from the DOM) and return its |
| // parent node, so we have a "bounding node" for the accessibility object. |
| VisiblePosition positionInTextControlInnerElement = coreObject->visiblePositionForIndex(0, true); |
| Node* innerMostNode = positionInTextControlInnerElement.deepEquivalent().anchorNode(); |
| if (!innerMostNode) |
| return 0; |
| |
| return innerMostNode->parentNode(); |
| } |
| |
| static void getSelectionOffsetsForObject(AccessibilityObject* coreObject, VisibleSelection& selection, gint& startOffset, gint& endOffset) |
| { |
| // Default values, unless the contrary is proved. |
| startOffset = 0; |
| endOffset = 0; |
| |
| Node* node = getNodeForAccessibilityObject(coreObject); |
| if (!node) |
| return; |
| |
| if (selection.isNone()) |
| return; |
| |
| // We need to limit our search to positions that fall inside the domain of the current object. |
| Position firstValidPosition = firstPositionInOrBeforeNode(node->firstDescendant()); |
| Position lastValidPosition = lastPositionInOrAfterNode(node->lastDescendant()); |
| |
| // Find the proper range for the selection that falls inside the object. |
| auto nodeRangeStart = std::max(selection.start(), firstValidPosition); |
| auto nodeRangeEnd = std::min(selection.end(), lastValidPosition); |
| |
| // Calculate position of the selected range inside the object. |
| Position parentFirstPosition = firstPositionInOrBeforeNode(node); |
| auto rangeInParent = *makeSimpleRange(parentFirstPosition, nodeRangeStart); |
| |
| // Set values for start offsets and calculate initial range length. |
| // These values might be adjusted later to cover special cases. |
| startOffset = webCoreOffsetToAtkOffset(coreObject, characterCount(rangeInParent, TextIteratorBehavior::EmitsCharactersBetweenAllVisiblePositions)); |
| auto nodeRange = *makeSimpleRange(nodeRangeStart, nodeRangeEnd); |
| int rangeLength = characterCount(nodeRange, TextIteratorBehavior::EmitsCharactersBetweenAllVisiblePositions); |
| |
| // Special cases that are only relevant when working with *_END boundaries. |
| if (selection.affinity() == Affinity::Upstream) { |
| VisiblePosition visibleStart(nodeRangeStart, Affinity::Upstream); |
| VisiblePosition visibleEnd(nodeRangeEnd, Affinity::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))); |
| } |
| |
| static gchar* webkitAccessibleTextGetText(AtkText* text, gint startOffset, gint endOffset) |
| { |
| g_return_val_if_fail(ATK_TEXT(text), 0); |
| returnValIfWebKitAccessibleIsInvalid(WEBKIT_ACCESSIBLE(text), 0); |
| |
| AccessibilityObject* coreObject = core(text); |
| |
| #if ENABLE(INPUT_TYPE_COLOR) |
| if (coreObject->roleValue() == AccessibilityRole::ColorWell) { |
| auto color = convertColor<SRGBA<float>>(coreObject->colorValue()); |
| return g_strdup_printf("rgb %7.5f %7.5f %7.5f 1", color.red, color.green, color.blue); |
| } |
| #endif |
| |
| String ret; |
| if (coreObject->isTextControl()) |
| ret = coreObject->doAXStringForRange(PlainTextRange(0, endOffset)); |
| else { |
| ret = coreObject->stringValue(); |
| if (!ret) |
| ret = coreObject->textUnderElement(AccessibilityTextUnderElementMode(AccessibilityTextUnderElementMode::TextUnderElementModeIncludeAllChildren)); |
| } |
| |
| // Prefix a item number/bullet if needed |
| int actualEndOffset = endOffset == -1 ? ret.length() : endOffset; |
| if (coreObject->roleValue() == AccessibilityRole::ListItem) { |
| RenderObject* objRenderer = coreObject->renderer(); |
| if (is<RenderListItem>(objRenderer)) { |
| String markerText = downcast<RenderListItem>(*objRenderer).markerTextWithSuffix().toString(); |
| ret = objRenderer->style().direction() == TextDirection::LTR ? markerText + ret : ret + markerText; |
| if (endOffset == -1) |
| actualEndOffset = ret.length() + markerText.length(); |
| } |
| } |
| |
| ret = ret.substring(startOffset, actualEndOffset - startOffset); |
| return g_strdup(ret.utf8().data()); |
| } |
| |
| enum GetTextRelativePosition { |
| GetTextPositionAt, |
| GetTextPositionBefore, |
| GetTextPositionAfter |
| }; |
| |
| // Convenience function to be used in early returns. |
| static char* emptyTextSelectionAtOffset(int offset, int* startOffset, int* endOffset) |
| { |
| *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--; |
| else if (textPosition == GetTextPositionAfter) |
| actualOffset++; |
| |
| GUniquePtr<char> textData(webkitAccessibleTextGetText(text, 0, -1)); |
| int textLength = g_utf8_strlen(textData.get(), -1); |
| |
| *startOffset = std::max(0, actualOffset); |
| *startOffset = std::min(*startOffset, textLength); |
| |
| *endOffset = std::max(0, actualOffset + 1); |
| *endOffset = std::min(*endOffset, textLength); |
| |
| if (*startOffset == *endOffset) |
| return g_strdup(""); |
| |
| 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) && deprecatedIsEditingWhitespace(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(Affinity::Upstream); |
| |
| return selectedWord; |
| } |
| |
| static int numberOfReplacedElementsBeforeOffset(AtkText* text, unsigned offset) |
| { |
| GUniquePtr<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 != notFound) { |
| index = textBeforeOffset.find(objectReplacementCharacter, index + 1); |
| count++; |
| } |
| return count; |
| } |
| |
| static char* webkitAccessibleTextWordForBoundary(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 bool isSentenceBoundary(const VisiblePosition &pos) |
| { |
| if (pos.isNull()) |
| return false; |
| |
| // It's definitely a sentence boundary if there's nothing before. |
| if (pos.previous().isNull()) |
| return true; |
| |
| // We go backwards and forward to make sure about this. |
| VisiblePosition startOfPreviousSentence = startOfSentence(pos); |
| return startOfPreviousSentence.isNotNull() && pos == endOfSentence(startOfPreviousSentence); |
| } |
| |
| static bool isWhiteSpaceBetweenSentences(const VisiblePosition& position) |
| { |
| if (position.isNull()) |
| return false; |
| |
| if (!deprecatedIsEditingWhitespace(position.characterAfter())) |
| return false; |
| |
| auto start = startOfWord(position, RightWordIfOnBoundary); |
| auto end = endOfWord(start, RightWordIfOnBoundary); |
| if (!isSentenceBoundary(start) && !isSentenceBoundary(end)) |
| return false; |
| |
| auto range = makeSimpleRange(start, end); |
| return range && contains<ComposedTree>(*range, makeBoundaryPoint(position)); |
| } |
| |
| static VisibleSelection sentenceAtPositionForAtkBoundary(const AccessibilityObject*, const VisiblePosition& position, AtkTextBoundary boundaryType) |
| { |
| VisiblePosition startPosition; |
| VisiblePosition endPosition; |
| |
| bool isAtStartOfSentenceForEndBoundary = isWhiteSpaceBetweenSentences(position) || isSentenceBoundary(position); |
| if (boundaryType == ATK_TEXT_BOUNDARY_SENTENCE_START || !isAtStartOfSentenceForEndBoundary) { |
| startPosition = isSentenceBoundary(position) ? position : startOfSentence(position); |
| // startOfSentence might stop at a linebreak in the HTML source code, |
| // but we don't want to stop there yet, so keep going. |
| while (!isSentenceBoundary(startPosition) && isHTMLLineBreak(startPosition.characterBefore())) |
| startPosition = startOfSentence(startPosition); |
| |
| endPosition = endOfSentence(startPosition); |
| } |
| |
| if (boundaryType == ATK_TEXT_BOUNDARY_SENTENCE_END) { |
| if (isAtStartOfSentenceForEndBoundary) { |
| startPosition = position; |
| endPosition = endOfSentence(endOfWord(position, RightWordIfOnBoundary)); |
| } |
| |
| // startOfSentence returns a position after any white space previous to |
| // the sentence, so we might need to adjust that offset for this boundary. |
| if (deprecatedIsEditingWhitespace(startPosition.characterBefore())) |
| startPosition = startOfWord(startPosition, LeftWordIfOnBoundary); |
| |
| // endOfSentence returns a position after any white space after the |
| // sentence, so we might need to adjust that offset for this boundary. |
| if (deprecatedIsEditingWhitespace(endPosition.characterBefore())) |
| endPosition = startOfWord(endPosition, LeftWordIfOnBoundary); |
| |
| // Finally, do some additional adjustments that might be needed if |
| // positions are at the start or the end of a line. |
| if (isStartOfLine(startPosition) && !isStartOfBlock(startPosition)) |
| startPosition = startPosition.previous(); |
| if (isStartOfLine(endPosition) && !isStartOfBlock(endPosition)) |
| endPosition = endPosition.previous(); |
| } |
| |
| VisibleSelection selectedSentence(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_SENTENCE_END) |
| selectedSentence.setAffinity(Affinity::Upstream); |
| |
| return selectedSentence; |
| } |
| |
| static char* webkitAccessibleTextSentenceForBoundary(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 currentSentence = sentenceAtPositionForAtkBoundary(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 sentence while already at the beginning. |
| if (isFirstVisiblePositionInNode(currentSentence.visibleStart(), node)) |
| return emptyTextSelectionAtOffset(0, startOffset, endOffset); |
| newPosition = currentSentence.visibleStart().previous(); |
| break; |
| |
| case GetTextPositionAfter: |
| // Early return if asking for the following word while already at the end. |
| if (isLastVisiblePositionInNode(currentSentence.visibleEnd(), node)) |
| return emptyTextSelectionAtOffset(accessibilityObjectLength(coreObject), startOffset, endOffset); |
| newPosition = currentSentence.visibleEnd().next(); |
| break; |
| |
| default: |
| ASSERT_NOT_REACHED(); |
| } |
| |
| // Determine the relevant sentence we are actually interested in |
| // and calculate the ATK offsets for it, then return everything. |
| VisibleSelection selectedSentence = newPosition != caretPosition ? sentenceAtPositionForAtkBoundary(coreObject, newPosition, boundaryType) : currentSentence; |
| getSelectionOffsetsForObject(coreObject, selectedSentence, *startOffset, *endOffset); |
| return webkitAccessibleTextGetText(text, *startOffset, *endOffset); |
| } |
| |
| static VisibleSelection lineAtPositionForAtkBoundary(const AccessibilityObject* coreObject, const VisiblePosition& position, AtkTextBoundary boundaryType) |
| { |
| UNUSED_PARAM(coreObject); |
| VisiblePosition startPosition; |
| VisiblePosition endPosition; |
| |
| switch (boundaryType) { |
| case ATK_TEXT_BOUNDARY_LINE_START: |
| startPosition = isStartOfLine(position) ? position : logicalStartOfLine(position); |
| endPosition = logicalEndOfLine(position); |
| |
| // In addition to checking that we are not at the end of a block, we need |
| // to check that endPosition has not UPSTREAM affinity, since that would |
| // cause trouble inside of text controls (we would be advancing too much). |
| if (!isEndOfBlock(endPosition) && endPosition.affinity() != Affinity::Upstream) |
| endPosition = endPosition.next(); |
| break; |
| |
| case ATK_TEXT_BOUNDARY_LINE_END: |
| startPosition = isEndOfLine(position) ? position : logicalStartOfLine(position); |
| if (!isStartOfBlock(startPosition)) |
| startPosition = startPosition.previous(); |
| endPosition = logicalEndOfLine(position); |
| break; |
| |
| default: |
| ASSERT_NOT_REACHED(); |
| } |
| |
| VisibleSelection selectedLine(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_LINE_END) |
| selectedLine.setAffinity(Affinity::Upstream); |
| |
| return selectedLine; |
| } |
| |
| static char* webkitAccessibleTextLineForBoundary(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 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 currentLine = lineAtPositionForAtkBoundary(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: |
| // No need to do additional work if we are using the "at" position, we just |
| // explicitly list this case option to catch invalid values in the default case. |
| break; |
| |
| case GetTextPositionBefore: |
| // Early return if asking for the previous line while already at the beginning. |
| if (isFirstVisiblePositionInNode(currentLine.visibleStart(), node)) |
| return emptyTextSelectionAtOffset(0, startOffset, endOffset); |
| newPosition = currentLine.visibleStart().previous(); |
| break; |
| |
| case GetTextPositionAfter: |
| // Early return if asking for the following word while already at the end. |
| if (isLastVisiblePositionInNode(currentLine.visibleEnd(), node)) |
| return emptyTextSelectionAtOffset(accessibilityObjectLength(coreObject), startOffset, endOffset); |
| newPosition = currentLine.visibleEnd().next(); |
| break; |
| |
| default: |
| ASSERT_NOT_REACHED(); |
| } |
| |
| // Determine the relevant line we are actually interested in |
| // and calculate the ATK offsets for it, then return everything. |
| VisibleSelection selectedLine = newPosition != caretPosition ? lineAtPositionForAtkBoundary(coreObject, newPosition, boundaryType) : currentLine; |
| getSelectionOffsetsForObject(coreObject, selectedLine, *startOffset, *endOffset); |
| |
| // We might need to adjust the start or end offset to include the list item marker, |
| // if present, when printing the first or the last full line for a list item. |
| RenderObject* renderer = coreObject->renderer(); |
| if (renderer->isListItem()) { |
| // For Left-to-Right, the list item marker is at the beginning of the exposed text. |
| if (renderer->style().direction() == TextDirection::LTR && isFirstVisiblePositionInNode(selectedLine.visibleStart(), node)) |
| *startOffset = 0; |
| |
| // For Right-to-Left, the list item marker is at the end of the exposed text. |
| if (renderer->style().direction() == TextDirection::RTL && isLastVisiblePositionInNode(selectedLine.visibleEnd(), node)) |
| *endOffset = accessibilityObjectLength(coreObject); |
| } |
| |
| return webkitAccessibleTextGetText(text, *startOffset, *endOffset); |
| } |
| |
| static gchar* webkitAccessibleTextGetTextForOffset(AtkText* text, gint offset, AtkTextBoundary boundaryType, GetTextRelativePosition textPosition, gint* startOffset, gint* endOffset) |
| { |
| AccessibilityObject* coreObject = core(text); |
| if (!coreObject || !coreObject->isAccessibilityRenderObject()) |
| return emptyTextSelectionAtOffset(0, startOffset, endOffset); |
| |
| switch (boundaryType) { |
| case ATK_TEXT_BOUNDARY_CHAR: |
| return webkitAccessibleTextGetChar(text, offset, textPosition, startOffset, endOffset); |
| |
| case ATK_TEXT_BOUNDARY_WORD_START: |
| case ATK_TEXT_BOUNDARY_WORD_END: |
| return webkitAccessibleTextWordForBoundary(text, offset, boundaryType, textPosition, startOffset, endOffset); |
| |
| case ATK_TEXT_BOUNDARY_LINE_START: |
| case ATK_TEXT_BOUNDARY_LINE_END: |
| return webkitAccessibleTextLineForBoundary(text, offset, boundaryType, textPosition, startOffset, endOffset); |
| |
| case ATK_TEXT_BOUNDARY_SENTENCE_START: |
| case ATK_TEXT_BOUNDARY_SENTENCE_END: |
| return webkitAccessibleTextSentenceForBoundary(text, offset, boundaryType, textPosition, startOffset, endOffset); |
| |
| default: |
| ASSERT_NOT_REACHED(); |
| } |
| |
| // This should never be reached. |
| return 0; |
| } |
| |
| static gchar* webkitAccessibleTextGetTextAfterOffset(AtkText* text, gint offset, AtkTextBoundary boundaryType, gint* startOffset, gint* endOffset) |
| { |
| g_return_val_if_fail(ATK_TEXT(text), 0); |
| returnValIfWebKitAccessibleIsInvalid(WEBKIT_ACCESSIBLE(text), 0); |
| |
| return webkitAccessibleTextGetTextForOffset(text, offset, boundaryType, GetTextPositionAfter, startOffset, endOffset); |
| } |
| |
| static gchar* webkitAccessibleTextGetTextAtOffset(AtkText* text, gint offset, AtkTextBoundary boundaryType, gint* startOffset, gint* endOffset) |
| { |
| g_return_val_if_fail(ATK_TEXT(text), 0); |
| returnValIfWebKitAccessibleIsInvalid(WEBKIT_ACCESSIBLE(text), 0); |
| |
| return webkitAccessibleTextGetTextForOffset(text, offset, boundaryType, GetTextPositionAt, startOffset, endOffset); |
| } |
| |
| static gchar* webkitAccessibleTextGetTextBeforeOffset(AtkText* text, gint offset, AtkTextBoundary boundaryType, gint* startOffset, gint* endOffset) |
| { |
| g_return_val_if_fail(ATK_TEXT(text), 0); |
| returnValIfWebKitAccessibleIsInvalid(WEBKIT_ACCESSIBLE(text), 0); |
| |
| return webkitAccessibleTextGetTextForOffset(text, offset, boundaryType, GetTextPositionBefore, startOffset, endOffset); |
| } |
| |
| static gunichar webkitAccessibleTextGetCharacterAtOffset(AtkText* text, gint) |
| { |
| g_return_val_if_fail(ATK_TEXT(text), 0); |
| returnValIfWebKitAccessibleIsInvalid(WEBKIT_ACCESSIBLE(text), 0); |
| |
| notImplemented(); |
| return 0; |
| } |
| |
| static gint webkitAccessibleTextGetCaretOffset(AtkText* text) |
| { |
| g_return_val_if_fail(ATK_TEXT(text), 0); |
| returnValIfWebKitAccessibleIsInvalid(WEBKIT_ACCESSIBLE(text), 0); |
| |
| // 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. |
| AXCoreObject* 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; |
| |
| return webCoreOffsetToAtkOffset(coreObject, offset); |
| } |
| |
| static AtkAttributeSet* webkitAccessibleTextGetRunAttributes(AtkText* text, gint offset, gint* startOffset, gint* endOffset) |
| { |
| g_return_val_if_fail(ATK_TEXT(text), 0); |
| returnValIfWebKitAccessibleIsInvalid(WEBKIT_ACCESSIBLE(text), 0); |
| |
| 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 = getRunAttributesFromAccessibilityObject(coreObject, offset, startOffset, endOffset); |
| |
| if (*startOffset < 0) { |
| *startOffset = offset; |
| *endOffset = offset; |
| } |
| |
| return result; |
| } |
| |
| static AtkAttributeSet* webkitAccessibleTextGetDefaultAttributes(AtkText* text) |
| { |
| g_return_val_if_fail(ATK_TEXT(text), 0); |
| returnValIfWebKitAccessibleIsInvalid(WEBKIT_ACCESSIBLE(text), 0); |
| |
| 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) |
| { |
| g_return_if_fail(ATK_TEXT(text)); |
| returnIfWebKitAccessibleIsInvalid(WEBKIT_ACCESSIBLE(text)); |
| |
| 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) |
| { |
| g_return_if_fail(ATK_TEXT(text)); |
| returnIfWebKitAccessibleIsInvalid(WEBKIT_ACCESSIBLE(text)); |
| |
| 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) |
| { |
| g_return_val_if_fail(ATK_TEXT(text), 0); |
| returnValIfWebKitAccessibleIsInvalid(WEBKIT_ACCESSIBLE(text), 0); |
| |
| return accessibilityObjectLength(core(text)); |
| } |
| |
| static gint webkitAccessibleTextGetOffsetAtPoint(AtkText* text, gint x, gint y, AtkCoordType) |
| { |
| g_return_val_if_fail(ATK_TEXT(text), 0); |
| returnValIfWebKitAccessibleIsInvalid(WEBKIT_ACCESSIBLE(text), 0); |
| |
| // 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) |
| { |
| g_return_val_if_fail(ATK_TEXT(text), 0); |
| returnValIfWebKitAccessibleIsInvalid(WEBKIT_ACCESSIBLE(text), 0); |
| |
| 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) |
| { |
| g_return_val_if_fail(ATK_TEXT(text), 0); |
| returnValIfWebKitAccessibleIsInvalid(WEBKIT_ACCESSIBLE(text), 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* text, gint, gint) |
| { |
| g_return_val_if_fail(ATK_TEXT(text), FALSE); |
| returnValIfWebKitAccessibleIsInvalid(WEBKIT_ACCESSIBLE(text), FALSE); |
| |
| notImplemented(); |
| return FALSE; |
| } |
| |
| static gboolean webkitAccessibleTextSetSelection(AtkText* text, gint selectionNum, gint startOffset, gint endOffset) |
| { |
| g_return_val_if_fail(ATK_TEXT(text), FALSE); |
| returnValIfWebKitAccessibleIsInvalid(WEBKIT_ACCESSIBLE(text), FALSE); |
| |
| // 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. |
| int offsetAdjustment = offsetAdjustmentForListItem(coreObject); |
| if (offsetAdjustment) { |
| if (startOffset < offsetAdjustment || endOffset < offsetAdjustment) |
| return FALSE; |
| |
| startOffset = atkOffsetToWebCoreOffset(text, startOffset); |
| endOffset = atkOffsetToWebCoreOffset(text, endOffset); |
| } |
| |
| 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) |
| { |
| g_return_val_if_fail(ATK_TEXT(text), FALSE); |
| returnValIfWebKitAccessibleIsInvalid(WEBKIT_ACCESSIBLE(text), FALSE); |
| |
| // 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) |
| { |
| // Internally, setting the caret offset is equivalent to set a zero-length |
| // selection, so delegate in that implementation and void duplicated code. |
| return webkitAccessibleTextSetSelection(text, 0, offset, offset); |
| } |
| |
| static gchar* webkitAccessibleTextGetStringAtOffset(AtkText* text, gint offset, AtkTextGranularity granularity, gint* startOffset, gint* endOffset) |
| { |
| // This new API has been designed to simplify the AtkText interface and it has been |
| // designed to keep exactly the same behaviour the atk_text_get_text_at_text() for |
| // ATK_TEXT_BOUNDARY_*_START boundaries, so for now we just need to translate the |
| // granularity to the right old boundary and reuse the code for the old API. |
| // However, this should be simplified later on (and a lot of code removed) once |
| // WebKitGTK depends on ATK >= 2.9.4 *and* can safely assume that a version of |
| // AT-SPI2 new enough not to include the old APIs is being used. But until then, |
| // we will have to live with both the old and new APIs implemented here. |
| // FIXME: WebKit nowadays depends on much newer ATK and we can safely assume AT-SPI2 |
| // isn't ancient. But whoever wrote this code didn't use ATK_CHECK_VERSION() guards, |
| // so it's unclear what is supposed to be changed here. |
| AtkTextBoundary boundaryType = ATK_TEXT_BOUNDARY_CHAR; |
| switch (granularity) { |
| case ATK_TEXT_GRANULARITY_CHAR: |
| break; |
| |
| case ATK_TEXT_GRANULARITY_WORD: |
| boundaryType = ATK_TEXT_BOUNDARY_WORD_START; |
| break; |
| |
| case ATK_TEXT_GRANULARITY_SENTENCE: |
| boundaryType = ATK_TEXT_BOUNDARY_SENTENCE_START; |
| break; |
| |
| case ATK_TEXT_GRANULARITY_LINE: |
| boundaryType = ATK_TEXT_BOUNDARY_LINE_START; |
| break; |
| |
| case ATK_TEXT_GRANULARITY_PARAGRAPH: |
| // FIXME: This has not been a need with the old AtkText API, which means ATs won't |
| // need it yet for some time, so we can skip it for now. |
| notImplemented(); |
| return g_strdup(""); |
| |
| default: |
| ASSERT_NOT_REACHED(); |
| } |
| |
| return webkitAccessibleTextGetTextForOffset(text, offset, boundaryType, GetTextPositionAt, startOffset, endOffset); |
| } |
| |
| 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; |
| iface->get_string_at_offset = webkitAccessibleTextGetStringAtOffset; |
| } |
| |
| #endif |