| /* |
| * Copyright (C) 2021 Igalia S.L. |
| * |
| * 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 "AccessibilityObjectAtspi.h" |
| |
| #if ENABLE(ACCESSIBILITY) && USE(ATSPI) |
| #include "AXObjectCache.h" |
| #include "AccessibilityAtspi.h" |
| #include "AccessibilityAtspiEnums.h" |
| #include "AccessibilityObject.h" |
| #include "AccessibilityObjectInterface.h" |
| #include "Editing.h" |
| #include "PlatformScreen.h" |
| #include "RenderLayer.h" |
| #include "RenderListItem.h" |
| #include "RenderListMarker.h" |
| #include "SurrogatePairAwareTextIterator.h" |
| #include "TextIterator.h" |
| #include "VisibleUnits.h" |
| #include <gio/gio.h> |
| #include <wtf/unicode/CharacterNames.h> |
| |
| namespace WebCore { |
| |
| AccessibilityObjectAtspi::TextGranularity AccessibilityObjectAtspi::atspiBoundaryToTextGranularity(uint32_t boundaryType) |
| { |
| switch (boundaryType) { |
| case Atspi::TextBoundaryType::CharBoundary: |
| return TextGranularity::Character; |
| case Atspi::TextBoundaryType::WordStartBoundary: |
| return TextGranularity::WordStart; |
| case Atspi::TextBoundaryType::WordEndBoundary: |
| return TextGranularity::WordEnd; |
| case Atspi::TextBoundaryType::SentenceStartBoundary: |
| return TextGranularity::SentenceStart; |
| case Atspi::TextBoundaryType::SentenceEndBoundary: |
| return TextGranularity::SentenceEnd; |
| case Atspi::TextBoundaryType::LineStartBoundary: |
| return TextGranularity::LineStart; |
| case Atspi::TextBoundaryType::LineEndBoundary: |
| return TextGranularity::LineEnd; |
| } |
| RELEASE_ASSERT_NOT_REACHED(); |
| } |
| |
| AccessibilityObjectAtspi::TextGranularity AccessibilityObjectAtspi::atspiGranularityToTextGranularity(uint32_t boundaryType) |
| { |
| switch (boundaryType) { |
| case Atspi::TextGranularityType::CharGranularity: |
| return TextGranularity::Character; |
| case Atspi::TextGranularityType::WordGranularity: |
| return TextGranularity::WordStart; |
| case Atspi::TextGranularityType::SentenceGranularity: |
| return TextGranularity::SentenceStart; |
| case Atspi::TextGranularityType::LineGranularity: |
| return TextGranularity::LineStart; |
| case Atspi::TextGranularityType::ParagraphGranularity: |
| return TextGranularity::Paragraph; |
| } |
| RELEASE_ASSERT_NOT_REACHED(); |
| } |
| |
| GDBusInterfaceVTable AccessibilityObjectAtspi::s_textFunctions = { |
| // method_call |
| [](GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar* methodName, GVariant* parameters, GDBusMethodInvocation* invocation, gpointer userData) { |
| auto atspiObject = Ref { *static_cast<AccessibilityObjectAtspi*>(userData) }; |
| atspiObject->updateBackingStore(); |
| |
| if (!g_strcmp0(methodName, "GetStringAtOffset")) { |
| int offset; |
| uint32_t granularityType; |
| g_variant_get(parameters, "(iu)", &offset, &granularityType); |
| int start = 0, end = 0; |
| auto text = atspiObject->textAtOffset(offset, atspiGranularityToTextGranularity(granularityType), start, end); |
| g_dbus_method_invocation_return_value(invocation, g_variant_new("(sii)", text.isNull() ? "" : text.data(), start, end)); |
| } else if (!g_strcmp0(methodName, "GetText")) { |
| int start, end; |
| g_variant_get(parameters, "(ii)", &start, &end); |
| auto text = atspiObject->text(start, end); |
| g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", text.isNull() ? "" : text.data())); |
| } else if (!g_strcmp0(methodName, "SetCaretOffset")) { |
| int offset; |
| g_variant_get(parameters, "(i)", &offset); |
| g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", atspiObject->selectRange(offset, offset))); |
| } else if (!g_strcmp0(methodName, "GetTextBeforeOffset")) { |
| g_dbus_method_invocation_return_error_literal(invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, ""); |
| } else if (!g_strcmp0(methodName, "GetTextAtOffset")) { |
| int offset; |
| uint32_t boundaryType; |
| g_variant_get(parameters, "(iu)", &offset, &boundaryType); |
| int start = 0, end = 0; |
| auto text = atspiObject->textAtOffset(offset, atspiBoundaryToTextGranularity(boundaryType), start, end); |
| g_dbus_method_invocation_return_value(invocation, g_variant_new("(sii)", text.isNull() ? "" : text.data(), start, end)); |
| } else if (!g_strcmp0(methodName, "GetTextAfterOffset")) |
| g_dbus_method_invocation_return_error_literal(invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, ""); |
| else if (!g_strcmp0(methodName, "GetCharacterAtOffset")) { |
| int offset; |
| g_variant_get(parameters, "(i)", &offset); |
| g_dbus_method_invocation_return_value(invocation, g_variant_new("(i)", atspiObject->characterAtOffset(offset))); |
| } else if (!g_strcmp0(methodName, "GetAttributeValue")) { |
| int offset; |
| const char* name; |
| g_variant_get(parameters, "(i&s)", &offset, &name); |
| auto attributes = atspiObject->textAttributesWithUTF8Offset(offset); |
| g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", attributes.attributes.get(String::fromUTF8(name)).utf8().data())); |
| } else if (!g_strcmp0(methodName, "GetAttributes")) { |
| int offset; |
| g_variant_get(parameters, "(i)", &offset); |
| auto attributes = atspiObject->textAttributesWithUTF8Offset(offset); |
| GVariantBuilder builder = G_VARIANT_BUILDER_INIT(G_VARIANT_TYPE("a{ss}")); |
| for (const auto& it : attributes.attributes) |
| g_variant_builder_add(&builder, "{ss}", it.key.utf8().data(), it.value.utf8().data()); |
| g_dbus_method_invocation_return_value(invocation, g_variant_new("(a{ss}ii)", &builder, attributes.startOffset, attributes.endOffset)); |
| } else if (!g_strcmp0(methodName, "GetAttributeRun")) { |
| int offset; |
| gboolean includeDefaults; |
| g_variant_get(parameters, "(ib)", &offset, &includeDefaults); |
| auto attributes = atspiObject->textAttributesWithUTF8Offset(offset, includeDefaults); |
| GVariantBuilder builder = G_VARIANT_BUILDER_INIT(G_VARIANT_TYPE("a{ss}")); |
| for (const auto& it : attributes.attributes) |
| g_variant_builder_add(&builder, "{ss}", it.key.utf8().data(), it.value.utf8().data()); |
| g_dbus_method_invocation_return_value(invocation, g_variant_new("(a{ss}ii)", &builder, attributes.startOffset, attributes.endOffset)); |
| } else if (!g_strcmp0(methodName, "GetDefaultAttributes") || !g_strcmp0(methodName, "GetDefaultAttributeSet")) { |
| auto attributes = atspiObject->textAttributesWithUTF8Offset(); |
| GVariantBuilder builder = G_VARIANT_BUILDER_INIT(G_VARIANT_TYPE("a{ss}")); |
| for (const auto& it : attributes.attributes) |
| g_variant_builder_add(&builder, "{ss}", it.key.utf8().data(), it.value.utf8().data()); |
| g_dbus_method_invocation_return_value(invocation, g_variant_new("(a{ss})", &builder)); |
| } else if (!g_strcmp0(methodName, "GetCharacterExtents")) { |
| int offset; |
| uint32_t coordinateType; |
| g_variant_get(parameters, "(iu)", &offset, &coordinateType); |
| auto extents = atspiObject->textExtents(offset, offset + 1, coordinateType); |
| g_dbus_method_invocation_return_value(invocation, g_variant_new("(iiii)", extents.x(), extents.y(), extents.width(), extents.height())); |
| } else if (!g_strcmp0(methodName, "GetRangeExtents")) { |
| int start, end; |
| uint32_t coordinateType; |
| g_variant_get(parameters, "(iiu)", &start, &end, &coordinateType); |
| auto extents = atspiObject->textExtents(start, end, coordinateType); |
| g_dbus_method_invocation_return_value(invocation, g_variant_new("(iiii)", extents.x(), extents.y(), extents.width(), extents.height())); |
| } else if (!g_strcmp0(methodName, "GetOffsetAtPoint")) { |
| int x, y; |
| uint32_t coordinateType; |
| g_variant_get(parameters, "(iiu)", &x, &y, &coordinateType); |
| g_dbus_method_invocation_return_value(invocation, g_variant_new("(i)", atspiObject->offsetAtPoint(IntPoint(x, y), coordinateType))); |
| } else if (!g_strcmp0(methodName, "GetNSelections")) { |
| int start, end; |
| g_dbus_method_invocation_return_value(invocation, g_variant_new("(i)", atspiObject->selectionBounds(start, end) && start != end ? 1 : 0)); |
| } else if (!g_strcmp0(methodName, "GetSelection")) { |
| int selectionNumber; |
| g_variant_get(parameters, "(i)", &selectionNumber); |
| if (selectionNumber) |
| g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "Not a valid selection: %d", selectionNumber); |
| else { |
| int start = 0, end = 0; |
| if (atspiObject->selectionBounds(start, end) && start == end) |
| start = end = 0; |
| g_dbus_method_invocation_return_value(invocation, g_variant_new("(ii)", start, end)); |
| } |
| } else if (!g_strcmp0(methodName, "AddSelection")) { |
| int start, end; |
| g_variant_get(parameters, "(ii)", &start, &end); |
| g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", atspiObject->selectRange(start, end))); |
| } else if (!g_strcmp0(methodName, "SetSelection")) { |
| int start, end, selectionNumber; |
| g_variant_get(parameters, "(iii)", &selectionNumber, &start, &end); |
| if (selectionNumber) |
| g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "Not a valid selection: %d", selectionNumber); |
| else |
| g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", atspiObject->selectRange(start, end))); |
| } else if (!g_strcmp0(methodName, "RemoveSelection")) { |
| int selectionNumber; |
| int caretOffset = -1; |
| if (!selectionNumber) { |
| int start, end; |
| if (atspiObject->selectionBounds(start, end)) |
| caretOffset = end; |
| } |
| g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", caretOffset != -1 ? atspiObject->selectRange(caretOffset, caretOffset) : FALSE)); |
| } else if (!g_strcmp0(methodName, "ScrollSubstringTo")) { |
| int start, end; |
| uint32_t scrollType; |
| g_variant_get(parameters, "(iiu)", &start, &end, &scrollType); |
| g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", atspiObject->scrollToMakeVisible(start, end, scrollType))); |
| } else if (!g_strcmp0(methodName, "ScrollSubstringToPoint")) { |
| int start, end, x, y; |
| uint32_t coordinateType; |
| g_variant_get(parameters, "(iiuii)", &start, &end, &coordinateType, &x, &y); |
| g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", atspiObject->scrollToPoint(start, end, coordinateType, x, y))); |
| } else if (!g_strcmp0(methodName, "GetBoundedRanges") || !g_strcmp0(methodName, "ScrollSubstringToPoint")) |
| g_dbus_method_invocation_return_error_literal(invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, ""); |
| }, |
| // get_property |
| [](GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar* propertyName, GError** error, gpointer userData) -> GVariant* { |
| auto atspiObject = Ref { *static_cast<AccessibilityObjectAtspi*>(userData) }; |
| atspiObject->updateBackingStore(); |
| |
| if (!g_strcmp0(propertyName, "CharacterCount")) |
| return g_variant_new_int32(g_utf8_strlen(atspiObject->text().utf8().data(), -1)); |
| if (!g_strcmp0(propertyName, "CaretOffset")) { |
| int start = 0, end = 0; |
| return g_variant_new_int32(atspiObject->selectionBounds(start, end) ? end : -1); |
| } |
| |
| g_set_error(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Unknown property '%s'", propertyName); |
| return nullptr; |
| }, |
| // set_property, |
| nullptr, |
| // padding |
| nullptr |
| }; |
| |
| static Vector<unsigned, 128> offsetMapping(const String& text) |
| { |
| if (text.is8Bit()) |
| return { }; |
| |
| Vector<unsigned, 128> offsets; |
| SurrogatePairAwareTextIterator iterator(text.characters16(), 0, text.length(), text.length()); |
| UChar32 character; |
| unsigned clusterLength = 0; |
| unsigned i; |
| for (i = 0; iterator.consume(character, clusterLength); iterator.advance(clusterLength), ++i) { |
| for (unsigned j = 0; j < clusterLength; ++j) |
| offsets.append(i); |
| } |
| offsets.append(i++); |
| return offsets; |
| } |
| |
| static inline unsigned UTF16OffsetToUTF8(const Vector<unsigned, 128>& mapping, unsigned offset) |
| { |
| return mapping.isEmpty() ? offset : mapping[offset]; |
| } |
| |
| static inline unsigned UTF8OffsetToUTF16(const Vector<unsigned, 128>& mapping, unsigned offset) |
| { |
| if (mapping.isEmpty()) |
| return offset; |
| |
| for (unsigned i = offset; i < mapping.size(); ++i) { |
| if (mapping[i] == offset) |
| return i; |
| } |
| return mapping.size(); |
| } |
| |
| String AccessibilityObjectAtspi::text() const |
| { |
| if (!m_coreObject) |
| return { }; |
| |
| m_hasListMarkerAtStart = false; |
| |
| #if ENABLE(INPUT_TYPE_COLOR) |
| if (m_coreObject->roleValue() == AccessibilityRole::ColorWell) { |
| auto color = convertColor<SRGBA<float>>(m_coreObject->colorValue()).resolved(); |
| GUniquePtr<char> colorString(g_strdup_printf("rgb %7.5f %7.5f %7.5f 1", color.red, color.green, color.blue)); |
| return String::fromUTF8(colorString.get()); |
| } |
| #endif |
| |
| if (m_coreObject->isTextControl()) |
| return m_coreObject->doAXStringForRange({ 0, String::MaxLength }); |
| |
| auto value = m_coreObject->stringValue(); |
| if (!value.isNull()) |
| return value; |
| |
| auto text = m_coreObject->textUnderElement(AccessibilityTextUnderElementMode(AccessibilityTextUnderElementMode::TextUnderElementModeIncludeAllChildren)); |
| if (auto* renderer = m_coreObject->renderer()) { |
| if (is<RenderListItem>(*renderer) && downcast<RenderListItem>(*renderer).markerRenderer()) { |
| if (renderer->style().direction() == TextDirection::LTR) { |
| text = makeString(objectReplacementCharacter, text); |
| m_hasListMarkerAtStart = true; |
| } else |
| text = makeString(text, objectReplacementCharacter); |
| } |
| } |
| return text; |
| } |
| |
| unsigned AccessibilityObject::getLengthForTextRange() const |
| { |
| // FIXME: this should probably be in sync with AccessibilityObjectAtspi::text(). |
| unsigned textLength = text().length(); |
| if (textLength) |
| return textLength; |
| |
| Node* node = this->node(); |
| RenderObject* renderer = node ? node->renderer() : nullptr; |
| if (is<RenderText>(renderer)) |
| textLength = downcast<RenderText>(*renderer).text().length(); |
| |
| if (!textLength && allowsTextRanges()) |
| textLength = textUnderElement(AccessibilityTextUnderElementMode(AccessibilityTextUnderElementMode::TextUnderElementModeIncludeAllChildren)).length(); |
| |
| return textLength; |
| } |
| |
| bool AccessibilityObject::allowsTextRanges() const |
| { |
| return true; |
| } |
| |
| CString AccessibilityObjectAtspi::text(int startOffset, int endOffset) const |
| { |
| auto utf16Text = text(); |
| auto utf8Text = utf16Text.utf8(); |
| if (utf8Text.isNull()) |
| return { }; |
| |
| auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1)); |
| if (endOffset == -1) |
| endOffset = length; |
| |
| if (startOffset < 0 || endOffset < 0) |
| return { }; |
| |
| if (endOffset <= startOffset) |
| return { }; |
| |
| if (!startOffset && endOffset == length) |
| return utf8Text; |
| |
| GUniquePtr<char> substring(g_utf8_substring(utf8Text.data(), startOffset, endOffset)); |
| return substring.get(); |
| } |
| |
| static inline int adjustInputOffset(unsigned utf16Offset, bool hasListMarkerAtStart) |
| { |
| return hasListMarkerAtStart && utf16Offset ? utf16Offset - 1 : utf16Offset; |
| } |
| |
| static inline int adjustOutputOffset(unsigned utf16Offset, bool hasListMarkerAtStart) |
| { |
| return hasListMarkerAtStart ? utf16Offset + 1 : utf16Offset; |
| } |
| |
| void AccessibilityObjectAtspi::textInserted(const String& insertedText, const VisiblePosition& position) |
| { |
| if (!m_interfaces.contains(Interface::Text)) |
| return; |
| |
| auto utf16Text = text(); |
| auto utf8Text = utf16Text.utf8(); |
| auto utf16Offset = adjustOutputOffset(m_coreObject->indexForVisiblePosition(position), m_hasListMarkerAtStart); |
| auto mapping = offsetMapping(utf16Text); |
| auto offset = UTF16OffsetToUTF8(mapping, utf16Offset); |
| auto utf8InsertedText = insertedText.utf8(); |
| auto insertedTextLength = g_utf8_strlen(utf8InsertedText.data(), -1); |
| AccessibilityAtspi::singleton().textChanged(*this, "insert", WTFMove(utf8InsertedText), offset - insertedTextLength, insertedTextLength); |
| } |
| |
| void AccessibilityObjectAtspi::textDeleted(const String& deletedText, const VisiblePosition& position) |
| { |
| if (!m_interfaces.contains(Interface::Text)) |
| return; |
| |
| auto utf16Text = text(); |
| auto utf8Text = utf16Text.utf8(); |
| auto utf16Offset = adjustOutputOffset(m_coreObject->indexForVisiblePosition(position), m_hasListMarkerAtStart); |
| auto mapping = offsetMapping(utf16Text); |
| auto offset = UTF16OffsetToUTF8(mapping, utf16Offset); |
| auto utf8DeletedText = deletedText.utf8(); |
| auto deletedTextLength = g_utf8_strlen(utf8DeletedText.data(), -1); |
| AccessibilityAtspi::singleton().textChanged(*this, "delete", WTFMove(utf8DeletedText), offset, deletedTextLength); |
| } |
| |
| IntPoint AccessibilityObjectAtspi::boundaryOffset(unsigned utf16Offset, TextGranularity granularity) const |
| { |
| if (!m_coreObject) |
| return { }; |
| |
| VisiblePosition offsetPosition = m_coreObject->visiblePositionForIndex(adjustInputOffset(utf16Offset, m_hasListMarkerAtStart)); |
| VisiblePosition startPosition, endPostion; |
| switch (granularity) { |
| case TextGranularity::Character: |
| RELEASE_ASSERT_NOT_REACHED(); |
| case TextGranularity::WordStart: { |
| if (!utf16Offset && m_hasListMarkerAtStart) |
| return { 0, 1 }; |
| |
| startPosition = isStartOfWord(offsetPosition) && deprecatedIsEditingWhitespace(offsetPosition.characterBefore()) ? offsetPosition : startOfWord(offsetPosition, LeftWordIfOnBoundary); |
| endPostion = nextWordPosition(startPosition); |
| auto positionAfterSpacingAndFollowingWord = nextWordPosition(endPostion); |
| if (positionAfterSpacingAndFollowingWord != endPostion) { |
| auto previousPosition = previousWordPosition(positionAfterSpacingAndFollowingWord); |
| if (previousPosition == startPosition) |
| endPostion = positionAfterSpacingAndFollowingWord; |
| else |
| endPostion = previousPosition; |
| } |
| break; |
| } |
| case TextGranularity::WordEnd: { |
| if (!utf16Offset && m_hasListMarkerAtStart) |
| return { 0, 1 }; |
| |
| startPosition = previousWordPosition(offsetPosition); |
| auto positionBeforeSpacingAndPreviousWord = previousWordPosition(startPosition); |
| if (positionBeforeSpacingAndPreviousWord != startPosition) |
| startPosition = nextWordPosition(positionBeforeSpacingAndPreviousWord); |
| endPostion = endOfWord(offsetPosition); |
| break; |
| } |
| case TextGranularity::SentenceStart: { |
| startPosition = startOfSentence(offsetPosition); |
| endPostion = endOfSentence(startPosition); |
| if (offsetPosition == endPostion) { |
| startPosition = nextSentencePosition(startPosition); |
| endPostion = endOfSentence(startPosition); |
| } |
| break; |
| } |
| case TextGranularity::SentenceEnd: |
| startPosition = previousSentencePosition(offsetPosition); |
| endPostion = endOfSentence(offsetPosition); |
| break; |
| case TextGranularity::LineStart: |
| startPosition = logicalStartOfLine(offsetPosition); |
| endPostion = nextLinePosition(offsetPosition, 0); |
| break; |
| case TextGranularity::LineEnd: |
| startPosition = logicalStartOfLine(offsetPosition); |
| endPostion = logicalEndOfLine(offsetPosition); |
| break; |
| case TextGranularity::Paragraph: |
| startPosition = startOfParagraph(offsetPosition); |
| endPostion = endOfParagraph(offsetPosition); |
| break; |
| } |
| |
| auto startOffset = m_coreObject->indexForVisiblePosition(startPosition); |
| // For no word boundaries, include the list marker if start offset is 0. |
| if (!startOffset && m_hasListMarkerAtStart && (granularity == TextGranularity::WordStart || granularity == TextGranularity::WordEnd)) |
| startOffset = adjustOutputOffset(startOffset, m_hasListMarkerAtStart); |
| return { startOffset, adjustOutputOffset(m_coreObject->indexForVisiblePosition(endPostion), m_hasListMarkerAtStart) }; |
| } |
| |
| CString AccessibilityObjectAtspi::textAtOffset(int offset, TextGranularity granularity, int& startOffset, int& endOffset) const |
| { |
| auto utf16Text = text(); |
| auto utf8Text = utf16Text.utf8(); |
| if (utf8Text.isNull()) |
| return { }; |
| |
| auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1)); |
| if (offset < 0 || offset > length) |
| return { }; |
| |
| if (granularity == TextGranularity::Character) { |
| startOffset = offset; |
| endOffset = std::min(offset + 1, length); |
| } else { |
| auto mapping = offsetMapping(utf16Text); |
| auto utf16Offset = UTF8OffsetToUTF16(mapping, offset); |
| auto boundaryOffset = this->boundaryOffset(utf16Offset, granularity); |
| startOffset = UTF16OffsetToUTF8(mapping, std::max<int>(boundaryOffset.x(), 0)); |
| endOffset = UTF16OffsetToUTF8(mapping, std::min<int>(boundaryOffset.y(), utf16Text.length())); |
| } |
| |
| GUniquePtr<char> substring(g_utf8_substring(utf8Text.data(), startOffset, endOffset)); |
| return substring.get(); |
| } |
| |
| int AccessibilityObjectAtspi::characterAtOffset(int offset) const |
| { |
| auto utf8Text = text().utf8(); |
| if (utf8Text.isNull()) |
| return 0; |
| |
| auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1)); |
| if (offset < 0 || offset >= length) |
| return 0; |
| |
| return g_utf8_get_char(g_utf8_offset_to_pointer(utf8Text.data(), offset)); |
| } |
| |
| std::optional<unsigned> AccessibilityObjectAtspi::characterOffset(UChar character, int index) const |
| { |
| auto utf16Text = text(); |
| unsigned start = 0; |
| size_t offset; |
| while ((offset = utf16Text.find(character, start)) != notFound) { |
| start = offset + 1; |
| if (!index) |
| break; |
| index--; |
| } |
| |
| if (offset == notFound) |
| return std::nullopt; |
| |
| auto mapping = offsetMapping(utf16Text); |
| return UTF16OffsetToUTF8(mapping, offset); |
| } |
| |
| std::optional<unsigned> AccessibilityObjectAtspi::characterIndex(UChar character, unsigned offset) const |
| { |
| auto utf16Text = text(); |
| auto utf8Text = utf16Text.utf8(); |
| if (utf8Text.isNull()) |
| return std::nullopt; |
| |
| auto length = g_utf8_strlen(utf8Text.data(), -1); |
| if (offset >= length) |
| return std::nullopt; |
| |
| auto mapping = offsetMapping(utf16Text); |
| auto utf16Offset = UTF8OffsetToUTF16(mapping, offset); |
| if (utf16Text[utf16Offset] != character) |
| return std::nullopt; |
| |
| unsigned start = 0; |
| int index = -1; |
| size_t position; |
| while ((position = utf16Text.find(character, start)) != notFound) { |
| index++; |
| if (static_cast<unsigned>(position) == utf16Offset) |
| break; |
| start = position + 1; |
| } |
| |
| if (index == -1) |
| return std::nullopt; |
| |
| return index; |
| } |
| |
| IntRect AccessibilityObjectAtspi::boundsForRange(unsigned utf16Offset, unsigned length, uint32_t coordinateType) const |
| { |
| if (!m_coreObject) |
| return { }; |
| |
| auto extents = m_coreObject->doAXBoundsForRange(PlainTextRange(utf16Offset, length)); |
| |
| auto* frameView = m_coreObject->documentFrameView(); |
| if (!frameView) |
| return extents; |
| |
| switch (coordinateType) { |
| case Atspi::CoordinateType::ScreenCoordinates: |
| return frameView->contentsToScreen(extents); |
| case Atspi::CoordinateType::WindowCoordinates: |
| return frameView->contentsToWindow(extents); |
| case Atspi::CoordinateType::ParentCoordinates: |
| return extents; |
| } |
| |
| RELEASE_ASSERT_NOT_REACHED(); |
| } |
| |
| IntRect AccessibilityObjectAtspi::textExtents(int startOffset, int endOffset, uint32_t coordinateType) const |
| { |
| auto utf16Text = text(); |
| auto utf8Text = utf16Text.utf8(); |
| if (utf8Text.isNull()) |
| return { }; |
| |
| auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1)); |
| startOffset = std::clamp(startOffset, 0, length); |
| if (endOffset == -1) |
| endOffset = length; |
| else |
| endOffset = std::clamp(endOffset, 0, length); |
| if (endOffset <= startOffset) |
| return { }; |
| |
| auto mapping = offsetMapping(utf16Text); |
| auto utf16StartOffset = UTF8OffsetToUTF16(mapping, startOffset); |
| auto utf16EndOffset = UTF8OffsetToUTF16(mapping, endOffset); |
| return boundsForRange(utf16StartOffset, utf16EndOffset - utf16StartOffset, coordinateType); |
| } |
| |
| int AccessibilityObjectAtspi::offsetAtPoint(const IntPoint& point, uint32_t coordinateType) const |
| { |
| auto utf16Text = text(); |
| auto utf8Text = utf16Text.utf8(); |
| if (utf8Text.isNull()) |
| return -1; |
| |
| auto convertedPoint = point; |
| if (auto* frameView = m_coreObject->documentFrameView()) { |
| switch (coordinateType) { |
| case Atspi::CoordinateType::ScreenCoordinates: |
| convertedPoint = frameView->screenToContents(point); |
| break; |
| case Atspi::CoordinateType::WindowCoordinates: |
| convertedPoint = frameView->windowToContents(point); |
| break; |
| case Atspi::CoordinateType::ParentCoordinates: |
| break; |
| } |
| } |
| |
| auto position = m_coreObject->visiblePositionForPoint(convertedPoint); |
| if (position.isNull()) |
| return -1; |
| |
| auto utf16Offset = adjustOutputOffset(m_coreObject->indexForVisiblePosition(position), m_hasListMarkerAtStart); |
| if (utf16Offset == -1) |
| return -1; |
| |
| return UTF16OffsetToUTF8(offsetMapping(utf16Text), utf16Offset); |
| } |
| |
| IntPoint AccessibilityObjectAtspi::boundsForSelection(const VisibleSelection& selection) const |
| { |
| if (selection.isNone()) |
| return { -1, -1 }; |
| |
| Node* node = nullptr; |
| if (!m_coreObject->isNativeTextControl()) |
| node = m_coreObject->node(); |
| else { |
| auto positionInTextControlInnerElement = m_coreObject->visiblePositionForIndex(0); |
| if (auto* innerMostNode = positionInTextControlInnerElement.deepEquivalent().anchorNode()) |
| node = innerMostNode->parentNode(); |
| } |
| if (!node) |
| return { -1, -1 }; |
| |
| // We need to limit our search to positions that fall inside the domain of the current object. |
| auto firstValidPosition = firstPositionInOrBeforeNode(node->firstDescendant()); |
| auto lastValidPosition = lastPositionInOrAfterNode(node->lastDescendant()); |
| |
| if (!intersects(makeVisiblePositionRange(makeSimpleRange(firstValidPosition, lastValidPosition)), VisiblePositionRange(selection))) |
| return { -1, -1 }; |
| |
| // 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. |
| auto parentFirstPosition = firstPositionInOrBeforeNode(node); |
| auto rangeInParent = *makeSimpleRange(parentFirstPosition, nodeRangeStart); |
| |
| // Set values for start offsets and calculate initial range length. |
| int startOffset = characterCount(rangeInParent, TextIteratorBehavior::EmitsObjectReplacementCharacters); |
| auto nodeRange = *makeSimpleRange(nodeRangeStart, nodeRangeEnd); |
| int rangeLength = characterCount(nodeRange, TextIteratorBehavior::EmitsObjectReplacementCharacters); |
| return { startOffset, startOffset + rangeLength }; |
| } |
| |
| IntPoint AccessibilityObjectAtspi::selectedRange() const |
| { |
| if (!m_coreObject) |
| return { -1, -1 }; |
| |
| return boundsForSelection(m_coreObject->selection()); |
| } |
| |
| bool AccessibilityObjectAtspi::selectionBounds(int& startOffset, int& endOffset) const |
| { |
| auto utf16Text = text(); |
| auto utf8Text = utf16Text.utf8(); |
| if (utf8Text.isNull()) |
| return false; |
| |
| auto bounds = selectedRange(); |
| if (bounds.x() < 0) |
| return false; |
| |
| auto mapping = offsetMapping(utf16Text); |
| startOffset = UTF16OffsetToUTF8(mapping, bounds.x()); |
| endOffset = UTF16OffsetToUTF8(mapping, bounds.y()); |
| |
| auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1)); |
| endOffset = std::clamp(endOffset, 0, length); |
| if (endOffset < startOffset) { |
| startOffset = endOffset = 0; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void AccessibilityObjectAtspi::setSelectedRange(unsigned utf16Offset, unsigned length) |
| { |
| if (!m_coreObject) |
| return; |
| |
| auto range = m_coreObject->visiblePositionRangeForRange(PlainTextRange(utf16Offset, length)); |
| m_coreObject->setSelectedVisiblePositionRange(range); |
| } |
| |
| bool AccessibilityObjectAtspi::selectRange(int startOffset, int endOffset) |
| { |
| auto utf16Text = text(); |
| auto utf8Text = utf16Text.utf8(); |
| if (utf8Text.isNull()) |
| return false; |
| |
| auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1)); |
| startOffset = std::clamp(startOffset, 0, length); |
| if (endOffset == -1) |
| endOffset = length; |
| else |
| endOffset = std::clamp(endOffset, 0, length); |
| |
| auto mapping = offsetMapping(utf16Text); |
| auto utf16StartOffset = UTF8OffsetToUTF16(mapping, startOffset); |
| auto utf16EndOffset = startOffset == endOffset ? utf16StartOffset : UTF8OffsetToUTF16(mapping, endOffset); |
| setSelectedRange(utf16StartOffset, utf16EndOffset - utf16StartOffset); |
| |
| return true; |
| } |
| |
| void AccessibilityObjectAtspi::selectionChanged(const VisibleSelection& selection) |
| { |
| if (!m_interfaces.contains(Interface::Text)) |
| return; |
| |
| if (selection.isNone()) |
| return; |
| |
| auto utf16Text = text(); |
| auto utf8Text = utf16Text.utf8(); |
| if (utf8Text.isNull()) |
| return; |
| |
| auto bounds = boundsForSelection(selection); |
| if (bounds.y() < 0) |
| return; |
| |
| auto length = g_utf8_strlen(utf8Text.data(), -1); |
| auto mapping = offsetMapping(utf16Text); |
| auto caretOffset = UTF16OffsetToUTF8(mapping, bounds.y()); |
| if (caretOffset <= length) |
| AccessibilityAtspi::singleton().textCaretMoved(*this, caretOffset); |
| |
| if (selection.isRange()) |
| AccessibilityAtspi::singleton().textSelectionChanged(*this); |
| } |
| |
| AccessibilityObjectAtspi::TextAttributes AccessibilityObjectAtspi::textAttributes(std::optional<unsigned> utf16Offset, bool includeDefault) const |
| { |
| if (!m_coreObject || !m_coreObject->renderer()) |
| return { }; |
| |
| auto accessibilityTextAttributes = [this](AXCoreObject* axObject, const HashMap<String, String>& defaultAttributes) -> HashMap<String, String> { |
| HashMap<String, String> attributes; |
| auto& style = axObject->renderer()->style(); |
| |
| auto addAttributeIfNeeded = [&](const String& name, const String& value) { |
| if (defaultAttributes.isEmpty() || defaultAttributes.get(name) != value) |
| attributes.add(name, value); |
| }; |
| |
| auto bgColor = style.visitedDependentColor(CSSPropertyBackgroundColor); |
| if (bgColor.isValid() && bgColor.isVisible()) { |
| auto [r, g, b, a] = bgColor.toColorTypeLossy<SRGBA<uint8_t>>().resolved(); |
| addAttributeIfNeeded("bg-color"_s, makeString(r, ',', g, ',', b)); |
| } |
| |
| auto fgColor = style.visitedDependentColor(CSSPropertyColor); |
| if (fgColor.isValid() && fgColor.isVisible()) { |
| auto [r, g, b, a] = fgColor.toColorTypeLossy<SRGBA<uint8_t>>().resolved(); |
| addAttributeIfNeeded("fg-color"_s, makeString(r, ',', g, ',', b)); |
| } |
| |
| addAttributeIfNeeded("family-name"_s, style.fontCascade().firstFamily()); |
| addAttributeIfNeeded("size"_s, makeString(std::round(style.computedFontPixelSize() * 72 / WebCore::screenDPI()), "pt")); |
| addAttributeIfNeeded("weight"_s, makeString(static_cast<float>(style.fontCascade().weight()))); |
| addAttributeIfNeeded("style"_s, style.fontCascade().italic() ? "italic" : "normal"); |
| addAttributeIfNeeded("strikethrough"_s, style.textDecoration() & TextDecorationLine::LineThrough ? "true" : "false"); |
| addAttributeIfNeeded("underline"_s, style.textDecoration() & TextDecorationLine::Underline ? "single" : "none"); |
| addAttributeIfNeeded("invisible"_s, style.visibility() == Visibility::Hidden ? "true" : "false"); |
| addAttributeIfNeeded("editable"_s, m_coreObject->canSetValueAttribute() ? "true" : "false"); |
| addAttributeIfNeeded("direction"_s, style.direction() == TextDirection::LTR ? "ltr" : "rtl"); |
| |
| if (!style.textIndent().isUndefined()) |
| addAttributeIfNeeded("indent"_s, makeString(valueForLength(style.textIndent(), m_coreObject->size().width()).toInt())); |
| |
| switch (style.textAlign()) { |
| case TextAlignMode::Start: |
| case TextAlignMode::End: |
| break; |
| case TextAlignMode::Left: |
| case TextAlignMode::WebKitLeft: |
| addAttributeIfNeeded("justification"_s, "left"); |
| break; |
| case TextAlignMode::Right: |
| case TextAlignMode::WebKitRight: |
| addAttributeIfNeeded("justification"_s, "right"); |
| break; |
| case TextAlignMode::Center: |
| case TextAlignMode::WebKitCenter: |
| addAttributeIfNeeded("justification"_s, "center"); |
| break; |
| case TextAlignMode::Justify: |
| addAttributeIfNeeded("justification"_s, "fill"); |
| break; |
| } |
| |
| String invalidStatus = m_coreObject->invalidStatus(); |
| if (invalidStatus != "false") |
| addAttributeIfNeeded("invalid"_s, invalidStatus); |
| |
| String language = m_coreObject->language(); |
| if (!language.isEmpty()) |
| addAttributeIfNeeded("language"_s, language); |
| |
| return attributes; |
| }; |
| |
| auto defaultAttributes = accessibilityTextAttributes(m_coreObject, { }); |
| if (!utf16Offset) |
| return { defaultAttributes, -1, -1 }; |
| |
| if (is<RenderListMarker>(*m_coreObject->renderer())) |
| return { defaultAttributes, 0, static_cast<int>(m_coreObject->stringValue().length()) }; |
| |
| if (!m_coreObject->node()) |
| return { defaultAttributes, -1, -1 }; |
| |
| if (!*utf16Offset && m_hasListMarkerAtStart) { |
| // Always consider list marker an independent run. |
| auto attributes = accessibilityTextAttributes(m_coreObject->children()[0].get(), defaultAttributes); |
| if (!includeDefault) |
| return { attributes, 0, 1 }; |
| |
| for (const auto& it : attributes) |
| defaultAttributes.set(it.key, it.value); |
| return { defaultAttributes, 0, 1 }; |
| } |
| |
| VisiblePosition offsetPosition = m_coreObject->visiblePositionForIndex(adjustInputOffset(*utf16Offset, m_hasListMarkerAtStart)); |
| auto* childNode = offsetPosition.deepEquivalent().deprecatedNode(); |
| if (!childNode) |
| return { defaultAttributes, -1, -1 }; |
| |
| auto* childRenderer = childNode->renderer(); |
| if (!childRenderer) |
| return { defaultAttributes, -1, -1 }; |
| |
| auto* childAxObject = childRenderer->document().axObjectCache()->get(childRenderer); |
| if (!childAxObject || childAxObject == m_coreObject) |
| return { defaultAttributes, -1, -1 }; |
| |
| auto attributes = accessibilityTextAttributes(childAxObject, defaultAttributes); |
| auto firstValidPosition = firstPositionInOrBeforeNode(m_coreObject->node()->firstDescendant()); |
| auto lastValidPosition = lastPositionInOrAfterNode(m_coreObject->node()->lastDescendant()); |
| |
| auto* startRenderer = childRenderer; |
| auto startPosition = firstPositionInOrBeforeNode(startRenderer->node()); |
| for (RenderObject* r = childRenderer->previousInPreOrder(); r && startPosition > firstValidPosition; r = r->previousInPreOrder()) { |
| if (r->firstChildSlow()) |
| continue; |
| |
| auto childAttributes = accessibilityTextAttributes(r->document().axObjectCache()->get(r), defaultAttributes); |
| if (childAttributes != attributes) |
| break; |
| |
| startRenderer = r; |
| startPosition = firstPositionInOrBeforeNode(startRenderer->node()); |
| } |
| |
| auto* endRenderer = childRenderer; |
| auto endPosition = lastPositionInOrAfterNode(endRenderer->node()); |
| for (RenderObject* r = childRenderer->nextInPreOrder(); r && endPosition < lastValidPosition; r = r->nextInPreOrder()) { |
| if (r->firstChildSlow()) |
| continue; |
| |
| auto childAttributes = accessibilityTextAttributes(r->document().axObjectCache()->get(r), defaultAttributes); |
| if (childAttributes != attributes) |
| break; |
| |
| endRenderer = r; |
| endPosition = lastPositionInOrAfterNode(endRenderer->node()); |
| } |
| |
| auto startOffset = adjustOutputOffset(m_coreObject->indexForVisiblePosition(startPosition), m_hasListMarkerAtStart); |
| auto endOffset = adjustOutputOffset(m_coreObject->indexForVisiblePosition(endPosition), m_hasListMarkerAtStart); |
| if (!includeDefault) |
| return { attributes, startOffset, endOffset }; |
| |
| for (const auto& it : attributes) |
| defaultAttributes.set(it.key, it.value); |
| |
| return { defaultAttributes, startOffset, endOffset }; |
| } |
| |
| AccessibilityObjectAtspi::TextAttributes AccessibilityObjectAtspi::textAttributesWithUTF8Offset(std::optional<int> offset, bool includeDefault) const |
| { |
| auto utf16Text = text(); |
| auto utf8Text = utf16Text.utf8(); |
| if (utf8Text.isNull()) |
| return { }; |
| |
| auto mapping = offsetMapping(utf16Text); |
| std::optional<unsigned> utf16Offset; |
| if (offset) { |
| auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1)); |
| if (*offset < 0 || *offset >= length) |
| return { }; |
| |
| utf16Offset = UTF8OffsetToUTF16(mapping, *offset); |
| } |
| |
| auto attributes = textAttributes(utf16Offset, includeDefault); |
| if (attributes.startOffset != -1) |
| attributes.startOffset = UTF16OffsetToUTF8(mapping, attributes.startOffset); |
| if (attributes.endOffset != -1) |
| attributes.endOffset = UTF16OffsetToUTF8(mapping, attributes.endOffset); |
| |
| return attributes; |
| } |
| |
| void AccessibilityObjectAtspi::textAttributesChanged() |
| { |
| if (!m_interfaces.contains(Interface::Text)) |
| return; |
| |
| AccessibilityAtspi::singleton().textAttributesChanged(*this); |
| } |
| |
| bool AccessibilityObjectAtspi::scrollToMakeVisible(int startOffset, int endOffset, uint32_t scrollType) const |
| { |
| auto utf16Text = text(); |
| auto utf8Text = utf16Text.utf8(); |
| if (utf8Text.isNull()) |
| return false; |
| |
| auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1)); |
| if (startOffset < 0 || startOffset > length) |
| return false; |
| if (endOffset < 0 || endOffset > length) |
| return false; |
| if (endOffset < startOffset) |
| std::swap(startOffset, endOffset); |
| |
| auto mapping = offsetMapping(utf16Text); |
| auto utf16StartOffset = UTF8OffsetToUTF16(mapping, startOffset); |
| auto utf16EndOffset = UTF8OffsetToUTF16(mapping, endOffset); |
| if (!m_coreObject->renderer()) |
| return true; |
| |
| IntRect rect = m_coreObject->doAXBoundsForRange(PlainTextRange(utf16StartOffset, utf16EndOffset - utf16StartOffset)); |
| |
| if (m_coreObject->isScrollView()) { |
| if (auto* parent = m_coreObject->parentObject()) |
| parent->scrollToMakeVisible(); |
| } |
| |
| ScrollAlignment alignX; |
| ScrollAlignment alignY; |
| switch (scrollType) { |
| case Atspi::ScrollType::TopLeft: |
| alignX = ScrollAlignment::alignLeftAlways; |
| alignY = ScrollAlignment::alignTopAlways; |
| break; |
| case Atspi::ScrollType::BottomRight: |
| alignX = ScrollAlignment::alignRightAlways; |
| alignY = ScrollAlignment::alignBottomAlways; |
| break; |
| case Atspi::ScrollType::TopEdge: |
| case Atspi::ScrollType::BottomEdge: |
| // Align to a particular edge is not supported, it's always the closest edge. |
| alignX = ScrollAlignment::alignCenterIfNeeded; |
| alignY = ScrollAlignment::alignToEdgeIfNeeded; |
| break; |
| case Atspi::ScrollType::LeftEdge: |
| case Atspi::ScrollType::RightEdge: |
| // Align to a particular edge is not supported, it's always the closest edge. |
| alignX = ScrollAlignment::alignToEdgeIfNeeded; |
| alignY = ScrollAlignment::alignCenterIfNeeded; |
| break; |
| case Atspi::ScrollType::Anywhere: |
| alignX = ScrollAlignment::alignCenterIfNeeded; |
| alignY = ScrollAlignment::alignCenterIfNeeded; |
| break; |
| } |
| |
| m_coreObject->renderer()->scrollRectToVisible(rect, false, { SelectionRevealMode::Reveal, alignX, alignY, ShouldAllowCrossOriginScrolling::Yes }); |
| return true; |
| } |
| |
| bool AccessibilityObjectAtspi::scrollToPoint(int startOffset, int endOffset, uint32_t coordinateType, int x, int y) const |
| { |
| auto utf16Text = text(); |
| auto utf8Text = utf16Text.utf8(); |
| if (utf8Text.isNull()) |
| return false; |
| |
| auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1)); |
| if (startOffset < 0 || startOffset > length) |
| return false; |
| if (endOffset < 0 || endOffset > length) |
| return false; |
| if (endOffset < startOffset) |
| std::swap(startOffset, endOffset); |
| |
| auto mapping = offsetMapping(utf16Text); |
| auto utf16StartOffset = UTF8OffsetToUTF16(mapping, startOffset); |
| auto utf16EndOffset = UTF8OffsetToUTF16(mapping, endOffset); |
| IntPoint point(x, y); |
| if (coordinateType == Atspi::CoordinateType::ScreenCoordinates) { |
| if (auto* frameView = m_coreObject->documentFrameView()) |
| point = frameView->contentsToWindow(frameView->screenToContents(point)); |
| } |
| |
| IntRect rect = m_coreObject->doAXBoundsForRange(PlainTextRange(utf16StartOffset, utf16EndOffset - utf16StartOffset)); |
| point.move(-rect.x(), -rect.y()); |
| m_coreObject->scrollToGlobalPoint(point); |
| return true; |
| } |
| |
| } // namespace WebCore |
| |
| #endif // ENABLE(ACCESSIBILITY) && USE(ATSPI) |