Added: trunk/Source/WebCore/accessibility/atspi/AccessibilityObjectTextAtspi.cpp (0 => 284675)
--- trunk/Source/WebCore/accessibility/atspi/AccessibilityObjectTextAtspi.cpp (rev 0)
+++ trunk/Source/WebCore/accessibility/atspi/AccessibilityObjectTextAtspi.cpp 2021-10-22 08:51:03 UTC (rev 284675)
@@ -0,0 +1,1009 @@
+/*
+ * 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 "AccessibilityAtspiEnums.h"
+#include "AccessibilityObjectInterface.h"
+#include "Editing.h"
+#include "PlatformScreen.h"
+#include "SurrogatePairAwareTextIterator.h"
+#include "TextIterator.h"
+#include "VisibleUnits.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) {
+ RELEASE_ASSERT(!isMainThread());
+ 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* {
+ RELEASE_ASSERT(!isMainThread());
+ 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
+{
+ AXCoreObject* axObject = isMainThread() ? m_coreObject : m_axObject;
+ if (!axObject)
+ return { };
+
+#if ENABLE(INPUT_TYPE_COLOR)
+ if (axObject->roleValue() == AccessibilityRole::ColorWell) {
+ auto color = convertColor<SRGBA<float>>(axObject->colorValue());
+ 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 (axObject->isTextControl())
+ return axObject->doAXStringForRange({ 0, String::MaxLength });
+
+ auto value = axObject->stringValue();
+ if (!value.isNull())
+ return value;
+
+ if (axObject == m_coreObject)
+ return m_coreObject->textUnderElement(AccessibilityTextUnderElementMode(AccessibilityTextUnderElementMode::TextUnderElementModeIncludeAllChildren));
+
+ return Accessibility::retrieveValueFromMainThread<String>([this]() -> String {
+ if (m_coreObject)
+ m_coreObject->updateBackingStore();
+
+ if (!m_coreObject)
+ return { };
+ return m_coreObject->textUnderElement(AccessibilityTextUnderElementMode(AccessibilityTextUnderElementMode::TextUnderElementModeIncludeAllChildren)).isolatedCopy();
+ });
+}
+
+unsigned AccessibilityObject::getLengthForTextRange() const
+{
+ RELEASE_ASSERT(isMainThread());
+ // 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
+{
+ RELEASE_ASSERT(!isMainThread());
+ 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();
+}
+
+void AccessibilityObjectAtspi::textInserted(const String& insertedText, const VisiblePosition& position)
+{
+ RELEASE_ASSERT(isMainThread());
+ if (!m_isRegistered.load())
+ return;
+
+ if (!m_interfaces.contains(Interface::Text))
+ return;
+
+ auto utf16Text = text();
+ auto utf8Text = utf16Text.utf8();
+ auto utf16Offset = m_coreObject->indexForVisiblePosition(position);
+ auto mapping = offsetMapping(utf16Text);
+ auto offset = UTF16OffsetToUTF8(mapping, utf16Offset);
+ auto utf8InsertedText = insertedText.utf8();
+ auto insertedTextLength = g_utf8_strlen(utf8InsertedText.data(), -1);
+ root()->atspi().textChanged(*this, "insert", WTFMove(utf8InsertedText), offset - insertedTextLength, insertedTextLength);
+}
+
+void AccessibilityObjectAtspi::textDeleted(const String& deletedText, const VisiblePosition& position)
+{
+ RELEASE_ASSERT(isMainThread());
+ if (!m_isRegistered.load())
+ return;
+
+ if (!m_interfaces.contains(Interface::Text))
+ return;
+
+ auto utf16Text = text();
+ auto utf8Text = utf16Text.utf8();
+ auto utf16Offset = m_coreObject->indexForVisiblePosition(position);
+ auto mapping = offsetMapping(utf16Text);
+ auto offset = UTF16OffsetToUTF8(mapping, utf16Offset);
+ auto utf8DeletedText = deletedText.utf8();
+ auto deletedTextLength = g_utf8_strlen(utf8DeletedText.data(), -1);
+ root()->atspi().textChanged(*this, "delete", WTFMove(utf8DeletedText), offset, deletedTextLength);
+}
+
+IntPoint AccessibilityObjectAtspi::boundaryOffset(unsigned utf16Offset, TextGranularity granularity) const
+{
+ return Accessibility::retrieveValueFromMainThread<IntPoint>([this, utf16Offset, granularity]() -> IntPoint {
+ if (m_coreObject)
+ m_coreObject->updateBackingStore();
+
+ if (!m_coreObject)
+ return { };
+
+ VisiblePosition offsetPosition = m_coreObject->visiblePositionForIndex(utf16Offset);
+ VisiblePosition startPosition, endPostion;
+ switch (granularity) {
+ case TextGranularity::Character:
+ RELEASE_ASSERT_NOT_REACHED();
+ case TextGranularity::WordStart: {
+ 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: {
+ 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;
+ }
+ return { m_coreObject->indexForVisiblePosition(startPosition), m_coreObject->indexForVisiblePosition(endPostion) };
+ });
+}
+
+CString AccessibilityObjectAtspi::textAtOffset(int offset, TextGranularity granularity, int& startOffset, int& endOffset) const
+{
+ RELEASE_ASSERT(!isMainThread());
+ 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
+{
+ RELEASE_ASSERT(!isMainThread());
+ 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));
+}
+
+IntRect AccessibilityObjectAtspi::boundsForRange(unsigned utf16Offset, unsigned length, uint32_t coordinateType) const
+{
+ return Accessibility::retrieveValueFromMainThread<IntRect>([this, utf16Offset, length, coordinateType]() -> IntRect {
+ if (m_coreObject)
+ m_coreObject->updateBackingStore();
+
+ 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
+{
+ RELEASE_ASSERT(!isMainThread());
+ 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
+{
+ RELEASE_ASSERT(!isMainThread());
+ auto utf16Text = text();
+ auto utf8Text = utf16Text.utf8();
+ if (utf8Text.isNull())
+ return -1;
+
+ auto utf16Offset = Accessibility::retrieveValueFromMainThread<int>([this, &point, coordinateType]() -> int {
+ if (m_coreObject)
+ m_coreObject->updateBackingStore();
+
+ if (!m_coreObject)
+ 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;
+
+ return m_coreObject->indexForVisiblePosition(position);
+ });
+
+ if (utf16Offset == -1)
+ return -1;
+
+ return UTF16OffsetToUTF8(offsetMapping(utf16Text), utf16Offset);
+}
+
+IntPoint AccessibilityObjectAtspi::boundsForSelection(const VisibleSelection& selection) const
+{
+ RELEASE_ASSERT(isMainThread());
+ 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::EmitsCharactersBetweenAllVisiblePositions);
+ auto nodeRange = *makeSimpleRange(nodeRangeStart, nodeRangeEnd);
+ int rangeLength = characterCount(nodeRange, TextIteratorBehavior::EmitsCharactersBetweenAllVisiblePositions);
+ return { startOffset, startOffset + rangeLength };
+}
+
+IntPoint AccessibilityObjectAtspi::selectedRange() const
+{
+ return Accessibility::retrieveValueFromMainThread<IntPoint>([this]() -> IntPoint {
+ if (m_coreObject)
+ m_coreObject->updateBackingStore();
+
+ if (!m_coreObject)
+ return { -1, -1 };
+
+ return boundsForSelection(m_coreObject->selection());
+ });
+}
+
+bool AccessibilityObjectAtspi::selectionBounds(int& startOffset, int& endOffset) const
+{
+ RELEASE_ASSERT(!isMainThread());
+ 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)
+{
+ Accessibility::performFunctionOnMainThread([this, utf16Offset, length] {
+ if (m_coreObject)
+ m_coreObject->updateBackingStore();
+
+ if (!m_coreObject)
+ return;
+
+ auto range = m_coreObject->visiblePositionRangeForRange(PlainTextRange(utf16Offset, length));
+ m_coreObject->setSelectedVisiblePositionRange(range);
+ });
+}
+
+bool AccessibilityObjectAtspi::selectRange(int startOffset, int endOffset)
+{
+ RELEASE_ASSERT(!isMainThread());
+ 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)
+{
+ RELEASE_ASSERT(isMainThread());
+ if (!m_isRegistered.load())
+ return;
+
+ 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)
+ root()->atspi().textCaretMoved(*this, caretOffset);
+
+ if (selection.isRange())
+ root()->atspi().textSelectionChanged(*this);
+}
+
+AccessibilityObjectAtspi::TextAttributes AccessibilityObjectAtspi::textAttributes(std::optional<unsigned> utf16Offset, bool includeDefault) const
+{
+ return Accessibility::retrieveValueFromMainThread<TextAttributes>([this, utf16Offset, includeDefault]() -> TextAttributes {
+ if (m_coreObject)
+ m_coreObject->updateBackingStore();
+
+ if (!m_coreObject || !m_coreObject->renderer() || !m_coreObject->node())
+ 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.toSRGBALossy<uint8_t>();
+ addAttributeIfNeeded("bg-color"_s, makeString(r, ',', g, ',', b));
+ }
+
+ auto fgColor = style.visitedDependentColor(CSSPropertyColor);
+ if (fgColor.isValid() && fgColor.isVisible()) {
+ auto [r, g, b, a] = fgColor.toSRGBALossy<uint8_t>();
+ 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() & TextDecoration::LineThrough ? "true" : "false");
+ addAttributeIfNeeded("underline"_s, style.textDecoration() & TextDecoration::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 };
+
+ VisiblePosition offsetPosition = m_coreObject->visiblePositionForIndex(*utf16Offset);
+ 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());
+ }
+
+ if (!includeDefault)
+ return { attributes, m_coreObject->indexForVisiblePosition(startPosition), m_coreObject->indexForVisiblePosition(endPosition) };
+
+ for (const auto& it : attributes)
+ defaultAttributes.set(it.key, it.value);
+
+ return { defaultAttributes, m_coreObject->indexForVisiblePosition(startPosition), m_coreObject->indexForVisiblePosition(endPosition) };
+ });
+}
+
+AccessibilityObjectAtspi::TextAttributes AccessibilityObjectAtspi::textAttributesWithUTF8Offset(std::optional<int> offset, bool includeDefault) const
+{
+ RELEASE_ASSERT(!isMainThread());
+ 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()
+{
+ RELEASE_ASSERT(isMainThread());
+ if (!m_isRegistered.load())
+ return;
+
+ if (!m_interfaces.contains(Interface::Text))
+ return;
+
+ root()->atspi().textAttributesChanged(*this);
+}
+
+bool AccessibilityObjectAtspi::scrollToMakeVisible(int startOffset, int endOffset, uint32_t scrollType) const
+{
+ RELEASE_ASSERT(!isMainThread());
+ 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);
+ Accessibility::performFunctionOnMainThread([this, utf16StartOffset, utf16EndOffset, scrollType] {
+ if (m_coreObject)
+ m_coreObject->updateBackingStore();
+
+ if (!m_coreObject || !m_coreObject->renderer())
+ return;
+
+ 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
+{
+ RELEASE_ASSERT(!isMainThread());
+ 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);
+ Accessibility::performFunctionOnMainThread([this, utf16StartOffset, utf16EndOffset, coordinateType, x, y] {
+ if (m_coreObject)
+ m_coreObject->updateBackingStore();
+
+ if (!m_coreObject)
+ return;
+
+ 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)
Modified: trunk/Tools/TestWebKitAPI/Tests/WebKitGtk/TestWebKitAccessibility.cpp (284674 => 284675)
--- trunk/Tools/TestWebKitAPI/Tests/WebKitGtk/TestWebKitAccessibility.cpp 2021-10-22 08:44:57 UTC (rev 284674)
+++ trunk/Tools/TestWebKitAPI/Tests/WebKitGtk/TestWebKitAccessibility.cpp 2021-10-22 08:51:03 UTC (rev 284675)
@@ -34,7 +34,15 @@
}
};
+struct AtspiTextRangeDeleter {
+ void operator()(AtspiTextRange* range) const
+ {
+ g_boxed_free(ATSPI_TYPE_TEXT_RANGE, range);
+ }
+};
+
using UniqueAtspiEvent = std::unique_ptr<AtspiEvent, AtspiEventDeleter>;
+using UniqueAtspiTextRange = std::unique_ptr<AtspiTextRange, AtspiTextRangeDeleter>;
class AccessibilityTest : public WebViewTest {
public:
@@ -98,9 +106,10 @@
m_eventSource = nullptr;
}
- void startEventMonitor(AtspiAccessible* source, const Vector<const char*>& events)
+ void startEventMonitor(AtspiAccessible* source, Vector<CString>&& events)
{
m_eventMonitor.source = source;
+ m_eventMonitor.eventTypes = WTFMove(events);
m_eventMonitor.listener = adoptGRef(atspi_event_listener_new([](AtspiEvent* event, gpointer userData) {
auto* test = static_cast<AccessibilityTest*>(userData);
if (event->source == test->m_eventMonitor.source)
@@ -107,8 +116,8 @@
test->m_eventMonitor.events.append(static_cast<AtspiEvent*>(g_boxed_copy(ATSPI_TYPE_EVENT, event)));
}, this, nullptr));
- for (const auto* event : events)
- atspi_event_listener_register(m_eventMonitor.listener.get(), event, nullptr);
+ for (const auto& event : m_eventMonitor.eventTypes)
+ atspi_event_listener_register(m_eventMonitor.listener.get(), event.data(), nullptr);
}
Vector<UniqueAtspiEvent> stopEventMonitor(unsigned expectedEvents, std::optional<Seconds> timeout = std::nullopt)
@@ -119,10 +128,22 @@
g_main_context_iteration(nullptr, timeout ? FALSE : TRUE);
auto events = WTFMove(m_eventMonitor.events);
- m_eventMonitor = { nullptr, { }, nullptr };
+ for (const auto& event : m_eventMonitor.eventTypes)
+ atspi_event_listener_deregister(m_eventMonitor.listener.get(), event.data(), nullptr);
+ m_eventMonitor = { nullptr, { }, { }, nullptr };
return events;
}
+ static AtspiEvent* findEvent(const Vector<UniqueAtspiEvent>& events, const char* eventType)
+ {
+ for (const auto& event : events) {
+ if (!g_strcmp0(event->type, eventType))
+ return event.get();
+ }
+
+ return nullptr;
+ }
+
static unsigned stateSetSize(AtspiStateSet* stateSet)
{
GArray* states = atspi_state_set_get_states(stateSet);
@@ -136,6 +157,7 @@
struct {
GRefPtr<AtspiEventListener> listener;
+ Vector<CString> eventTypes;
Vector<UniqueAtspiEvent> events;
AtspiAccessible* source { nullptr };
} m_eventMonitor;
@@ -805,6 +827,694 @@
}
#endif
+static void testTextBasic(AccessibilityTest* test, gconstpointer)
+{
+ test->showInWindow();
+ test->loadHtml(
+ "<html>"
+ " <body>"
+ " <p>This is a line of text</p>"
+ " </body>"
+ "</html>",
+ nullptr);
+ test->waitUntilLoadFinished();
+
+ auto testApp = test->findTestApplication();
+ g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));
+
+ auto documentWeb = test->findDocumentWeb(testApp.get());
+ g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
+ g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);
+
+ auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
+ g_assert_true(ATSPI_IS_TEXT(p.get()));
+ auto length = atspi_text_get_character_count(ATSPI_TEXT(p.get()), nullptr);
+ g_assert_cmpint(length, ==, 22);
+ GUniquePtr<char> text(atspi_text_get_text(ATSPI_TEXT(p.get()), 0, length, nullptr));
+ g_assert_cmpstr(text.get(), ==, "This is a line of text");
+ text.reset(atspi_text_get_text(ATSPI_TEXT(p.get()), 0, -1, nullptr));
+ g_assert_cmpstr(text.get(), ==, "This is a line of text");
+ text.reset(atspi_text_get_text(ATSPI_TEXT(p.get()), 5, 7, nullptr));
+ g_assert_cmpstr(text.get(), ==, "is");
+ text.reset(atspi_text_get_text(ATSPI_TEXT(p.get()), 0, -2, nullptr));
+ g_assert_cmpstr(text.get(), ==, "");
+ text.reset(atspi_text_get_text(ATSPI_TEXT(p.get()), -5, 23, nullptr));
+ g_assert_cmpstr(text.get(), ==, "");
+}
+
+static void testTextSurrogatePair(AccessibilityTest* test, gconstpointer)
+{
+#if USE(ATK)
+ g_test_skip("Surrogate pairs are not correctly handled by WebKit when using ATK");
+ return;
+#endif
+
+ test->showInWindow();
+ test->loadHtml(
+ "<html>"
+ " <body>"
+ " <p>This contains a 𝌆 symbol</p>"
+ " <input value='This contains a 𝌆 symbol'/>"
+ " </body>"
+ "</html>",
+ nullptr);
+ test->waitUntilLoadFinished();
+
+ auto testApp = test->findTestApplication();
+ g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));
+
+ auto documentWeb = test->findDocumentWeb(testApp.get());
+ g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
+ g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 2);
+
+ auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
+ g_assert_true(ATSPI_IS_TEXT(p.get()));
+ auto length = atspi_text_get_character_count(ATSPI_TEXT(p.get()), nullptr);
+ g_assert_cmpint(length, ==, 24);
+ GUniquePtr<char> text(atspi_text_get_text(ATSPI_TEXT(p.get()), 0, length, nullptr));
+ g_assert_cmpstr(text.get(), ==, "This contains a 𝌆 symbol");
+ text.reset(atspi_text_get_text(ATSPI_TEXT(p.get()), 16, 17, nullptr));
+ g_assert_cmpstr(text.get(), ==, "𝌆");
+
+ auto section = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
+ g_assert_true(ATSPI_IS_ACCESSIBLE(section.get()));
+ g_assert_cmpint(atspi_accessible_get_child_count(section.get(), nullptr), ==, 1);
+ auto input = adoptGRef(atspi_accessible_get_child_at_index(section.get(), 0, nullptr));
+ g_assert_true(ATSPI_IS_TEXT(input.get()));
+ length = atspi_text_get_character_count(ATSPI_TEXT(input.get()), nullptr);
+ g_assert_cmpint(length, ==, 24);
+ text.reset(atspi_text_get_text(ATSPI_TEXT(input.get()), 0, length, nullptr));
+ g_assert_cmpstr(text.get(), ==, "This contains a 𝌆 symbol");
+ text.reset(atspi_text_get_text(ATSPI_TEXT(input.get()), 16, 17, nullptr));
+ g_assert_cmpstr(text.get(), ==, "𝌆");
+}
+
+static void testTextIterator(AccessibilityTest* test, gconstpointer)
+{
+ test->showInWindow(800, 600);
+ test->loadHtml(
+ "<html>"
+ " <body>"
+ " <p>Text of first sentence. This is the second sentence.<br>And this is the next paragraph.</p>"
+ " </body>"
+ "</html>",
+ nullptr);
+ test->waitUntilLoadFinished();
+
+ auto testApp = test->findTestApplication();
+ g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));
+
+ auto documentWeb = test->findDocumentWeb(testApp.get());
+ g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
+ g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);
+
+ auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
+ g_assert_true(ATSPI_IS_TEXT(p.get()));
+ auto length = atspi_text_get_character_count(ATSPI_TEXT(p.get()), nullptr);
+ g_assert_cmpint(length, ==, 84);
+ GUniquePtr<char> text(atspi_text_get_text(ATSPI_TEXT(p.get()), 0, length, nullptr));
+ g_assert_cmpstr(text.get(), ==, "Text of first sentence. This is the second sentence.\nAnd this is the next paragraph.");
+
+ // Character granularity.
+ UniqueAtspiTextRange range(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 0, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
+ g_assert_cmpstr(range->content, ==, "T");
+ g_assert_cmpint(range->start_offset, ==, 0);
+ g_assert_cmpint(range->end_offset, ==, 1);
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 83, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
+ g_assert_cmpstr(range->content, ==, ".");
+ g_assert_cmpint(range->start_offset, ==, 83);
+ g_assert_cmpint(range->end_offset, ==, 84);
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 84, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
+ g_assert_cmpstr(range->content, ==, "");
+ g_assert_cmpint(range->start_offset, ==, 84);
+ g_assert_cmpint(range->end_offset, ==, 84);
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 85, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
+ g_assert_cmpstr(range->content, ==, "");
+#if USE(ATSPI)
+ g_assert_cmpint(range->start_offset, ==, 0);
+ g_assert_cmpint(range->end_offset, ==, 0);
+#else
+ // FIXME: ATK returns wrong offsets in this case.
+ g_assert_cmpint(range->start_offset, ==, 84);
+ g_assert_cmpint(range->end_offset, ==, 84);
+#endif
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), -1, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
+ g_assert_cmpstr(range->content, ==, "");
+ g_assert_cmpint(range->start_offset, ==, 0);
+ g_assert_cmpint(range->end_offset, ==, 0);
+
+ // Word granularity.
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 0, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
+ g_assert_cmpstr(range->content, ==, "Text ");
+ g_assert_cmpint(range->start_offset, ==, 0);
+ g_assert_cmpint(range->end_offset, ==, 5);
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 4, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
+ g_assert_cmpstr(range->content, ==, "Text ");
+ g_assert_cmpint(range->start_offset, ==, 0);
+ g_assert_cmpint(range->end_offset, ==, 5);
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 5, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
+ g_assert_cmpstr(range->content, ==, "of ");
+ g_assert_cmpint(range->start_offset, ==, 5);
+ g_assert_cmpint(range->end_offset, ==, 8);
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 40, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
+ g_assert_cmpstr(range->content, ==, "second ");
+ g_assert_cmpint(range->start_offset, ==, 36);
+ g_assert_cmpint(range->end_offset, ==, 43);
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 16, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
+ g_assert_cmpstr(range->content, ==, "sentence. ");
+ g_assert_cmpint(range->start_offset, ==, 14);
+ g_assert_cmpint(range->end_offset, ==, 24);
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 74, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
+ g_assert_cmpstr(range->content, ==, "paragraph.");
+ g_assert_cmpint(range->start_offset, ==, 74);
+ g_assert_cmpint(range->end_offset, ==, 84);
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 83, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
+ g_assert_cmpstr(range->content, ==, "paragraph.");
+ g_assert_cmpint(range->start_offset, ==, 74);
+ g_assert_cmpint(range->end_offset, ==, 84);
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 80, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
+ g_assert_cmpstr(range->content, ==, "paragraph.");
+ g_assert_cmpint(range->start_offset, ==, 74);
+ g_assert_cmpint(range->end_offset, ==, 84);
+
+ // Sentence granularity.
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 0, ATSPI_TEXT_GRANULARITY_SENTENCE, nullptr));
+ g_assert_cmpstr(range->content, ==, "Text of first sentence. ");
+ g_assert_cmpint(range->start_offset, ==, 0);
+ g_assert_cmpint(range->end_offset, ==, 24);
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 23, ATSPI_TEXT_GRANULARITY_SENTENCE, nullptr));
+ g_assert_cmpstr(range->content, ==, "Text of first sentence. ");
+ g_assert_cmpint(range->start_offset, ==, 0);
+ g_assert_cmpint(range->end_offset, ==, 24);
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 24, ATSPI_TEXT_GRANULARITY_SENTENCE, nullptr));
+ g_assert_cmpstr(range->content, ==, "This is the second sentence.\n");
+ g_assert_cmpint(range->start_offset, ==, 24);
+ g_assert_cmpint(range->end_offset, ==, 53);
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 40, ATSPI_TEXT_GRANULARITY_SENTENCE, nullptr));
+ g_assert_cmpstr(range->content, ==, "This is the second sentence.\n");
+ g_assert_cmpint(range->start_offset, ==, 24);
+ g_assert_cmpint(range->end_offset, ==, 53);
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 53, ATSPI_TEXT_GRANULARITY_SENTENCE, nullptr));
+ g_assert_cmpstr(range->content, ==, "And this is the next paragraph.");
+ g_assert_cmpint(range->start_offset, ==, 53);
+ g_assert_cmpint(range->end_offset, ==, 84);
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 83, ATSPI_TEXT_GRANULARITY_SENTENCE, nullptr));
+ g_assert_cmpstr(range->content, ==, "And this is the next paragraph.");
+ g_assert_cmpint(range->start_offset, ==, 53);
+ g_assert_cmpint(range->end_offset, ==, 84);
+
+ // Line granularity.
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 0, ATSPI_TEXT_GRANULARITY_LINE, nullptr));
+ g_assert_cmpstr(range->content, ==, "Text of first sentence. This is the second sentence.\n");
+ g_assert_cmpint(range->start_offset, ==, 0);
+ g_assert_cmpint(range->end_offset, ==, 53);
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 83, ATSPI_TEXT_GRANULARITY_LINE, nullptr));
+ g_assert_cmpstr(range->content, ==, "And this is the next paragraph.");
+ g_assert_cmpint(range->start_offset, ==, 53);
+ g_assert_cmpint(range->end_offset, ==, 84);
+
+#if USE(ATSPI) // ATK doesn't implement paragraph granularity.
+ // Paragraph granularity.
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 0, ATSPI_TEXT_GRANULARITY_PARAGRAPH, nullptr));
+ g_assert_cmpstr(range->content, ==, "Text of first sentence. This is the second sentence.");
+ g_assert_cmpint(range->start_offset, ==, 0);
+ g_assert_cmpint(range->end_offset, ==, 52);
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 83, ATSPI_TEXT_GRANULARITY_PARAGRAPH, nullptr));
+ g_assert_cmpstr(range->content, ==, "And this is the next paragraph.");
+ g_assert_cmpint(range->start_offset, ==, 53);
+ g_assert_cmpint(range->end_offset, ==, 84);
+#endif
+
+ // Using a text control now.
+ test->loadHtml(
+ "<html>"
+ " <body>"
+ " <input value='Text of input field'/>"
+ " </body>"
+ "</html>",
+ nullptr);
+ test->waitUntilLoadFinished();
+
+ documentWeb = test->findDocumentWeb(testApp.get());
+ g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
+ g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);
+
+ auto section = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
+ g_assert_true(ATSPI_IS_ACCESSIBLE(section.get()));
+ g_assert_cmpint(atspi_accessible_get_child_count(section.get(), nullptr), ==, 1);
+ auto input = adoptGRef(atspi_accessible_get_child_at_index(section.get(), 0, nullptr));
+ g_assert_true(ATSPI_IS_TEXT(input.get()));
+ length = atspi_text_get_character_count(ATSPI_TEXT(input.get()), nullptr);
+ g_assert_cmpint(length, ==, 19);
+ text.reset(atspi_text_get_text(ATSPI_TEXT(input.get()), 0, length, nullptr));
+ g_assert_cmpstr(text.get(), ==, "Text of input field");
+
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(input.get()), 0, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
+ g_assert_cmpstr(range->content, ==, "T");
+ g_assert_cmpint(range->start_offset, ==, 0);
+ g_assert_cmpint(range->end_offset, ==, 1);
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(input.get()), 18, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
+ g_assert_cmpstr(range->content, ==, "d");
+ g_assert_cmpint(range->start_offset, ==, 18);
+ g_assert_cmpint(range->end_offset, ==, 19);
+
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(input.get()), 0, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
+ g_assert_cmpstr(range->content, ==, "Text ");
+ g_assert_cmpint(range->start_offset, ==, 0);
+ g_assert_cmpint(range->end_offset, ==, 5);
+ range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(input.get()), 16, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
+ g_assert_cmpstr(range->content, ==, "field");
+ g_assert_cmpint(range->start_offset, ==, 14);
+ g_assert_cmpint(range->end_offset, ==, 19);
+}
+
+static void testTextExtents(AccessibilityTest* test, gconstpointer)
+{
+ test->showInWindow();
+ test->loadHtml(
+ "<html>"
+ " <body>"
+ " <p>Text</p>"
+ " </body>"
+ "</html>",
+ nullptr);
+ test->waitUntilLoadFinished();
+
+ auto testApp = test->findTestApplication();
+ g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));
+
+ auto documentWeb = test->findDocumentWeb(testApp.get());
+ g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
+ g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);
+
+ auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
+ g_assert_true(ATSPI_IS_TEXT(p.get()));
+ GUniquePtr<AtspiRect> firstCharRect(atspi_text_get_character_extents(ATSPI_TEXT(p.get()), 0, ATSPI_COORD_TYPE_WINDOW, nullptr));
+ g_assert_nonnull(firstCharRect.get());
+ g_assert_cmpuint(firstCharRect->x, >, 0);
+ g_assert_cmpuint(firstCharRect->y, >, 0);
+ g_assert_cmpuint(firstCharRect->width, >, 0);
+ g_assert_cmpuint(firstCharRect->height, >, 0);
+ GUniquePtr<AtspiRect> lastCharRect(atspi_text_get_character_extents(ATSPI_TEXT(p.get()), 3, ATSPI_COORD_TYPE_WINDOW, nullptr));
+ g_assert_nonnull(lastCharRect.get());
+ g_assert_cmpuint(lastCharRect->x, >, firstCharRect->x);
+ g_assert_cmpuint(lastCharRect->y, ==, firstCharRect->y);
+ g_assert_cmpuint(lastCharRect->width, >, 0);
+ g_assert_cmpuint(lastCharRect->height, >, 0);
+ GUniquePtr<AtspiRect> rangeRect(atspi_text_get_range_extents(ATSPI_TEXT(p.get()), 0, 4, ATSPI_COORD_TYPE_WINDOW, nullptr));
+ g_assert_nonnull(rangeRect.get());
+ g_assert_cmpuint(rangeRect->x, ==, firstCharRect->x);
+ g_assert_cmpuint(rangeRect->y, ==, firstCharRect->y);
+ g_assert_cmpuint(rangeRect->width, >, 0);
+ g_assert_cmpuint(rangeRect->height, >, 0);
+#if USE(ATSPI) // Offset at point is broken with ATK.
+ auto offset = atspi_text_get_offset_at_point(ATSPI_TEXT(p.get()), firstCharRect->x, firstCharRect->y, ATSPI_COORD_TYPE_WINDOW, nullptr);
+ g_assert_cmpint(offset, ==, 0);
+ offset = atspi_text_get_offset_at_point(ATSPI_TEXT(p.get()), firstCharRect->x + firstCharRect->width, firstCharRect->y, ATSPI_COORD_TYPE_WINDOW, nullptr);
+ g_assert_cmpint(offset, ==, 1);
+ offset = atspi_text_get_offset_at_point(ATSPI_TEXT(p.get()), lastCharRect->x, lastCharRect->y, ATSPI_COORD_TYPE_WINDOW, nullptr);
+ g_assert_cmpint(offset, ==, 3);
+ offset = atspi_text_get_offset_at_point(ATSPI_TEXT(p.get()), lastCharRect->x + lastCharRect->width, lastCharRect->y, ATSPI_COORD_TYPE_WINDOW, nullptr);
+ g_assert_cmpint(offset, ==, 4);
+#endif
+}
+
+static void testTextSelections(AccessibilityTest* test, gconstpointer)
+{
+ test->showInWindow(800, 600);
+ test->loadHtml(
+ "<html>"
+ " <body>"
+ " <p>This is a line of text</p>"
+ " <input value='This is text input value'/>"
+ " </body>"
+ "</html>",
+ nullptr);
+ test->waitUntilLoadFinished();
+
+ auto testApp = test->findTestApplication();
+ g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));
+
+ auto documentWeb = test->findDocumentWeb(testApp.get());
+ g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
+ g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 2);
+
+ auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
+ g_assert_true(ATSPI_IS_TEXT(p.get()));
+
+ auto selectionCount = atspi_text_get_n_selections(ATSPI_TEXT(p.get()), nullptr);
+ g_assert_cmpuint(selectionCount, ==, 0);
+ auto caretOffset = atspi_text_get_caret_offset(ATSPI_TEXT(p.get()), nullptr);
+#if USE(ATSPI)
+ g_assert_cmpint(caretOffset, ==, -1);
+#else
+ g_assert_cmpint(caretOffset, ==, 0);
+#endif
+
+ GUniquePtr<AtspiRange> selection(atspi_text_get_selection(ATSPI_TEXT(p.get()), 0, nullptr));
+ g_assert_nonnull(selection.get());
+ g_assert_cmpint(selection->start_offset, ==, 0);
+ g_assert_cmpint(selection->end_offset, ==, 0);
+
+ g_assert_true(atspi_text_set_selection(ATSPI_TEXT(p.get()), 0, 5, 14, nullptr));
+ selectionCount = atspi_text_get_n_selections(ATSPI_TEXT(p.get()), nullptr);
+ g_assert_cmpuint(selectionCount, ==, 1);
+ caretOffset = atspi_text_get_caret_offset(ATSPI_TEXT(p.get()), nullptr);
+ g_assert_cmpint(caretOffset, ==, 14);
+ selection.reset(atspi_text_get_selection(ATSPI_TEXT(p.get()), 0, nullptr));
+ g_assert_nonnull(selection.get());
+ g_assert_cmpint(selection->start_offset, ==, 5);
+ g_assert_cmpint(selection->end_offset, ==, 14);
+ GUniquePtr<char> text(atspi_text_get_text(ATSPI_TEXT(p.get()), selection->start_offset, selection->end_offset, nullptr));
+ g_assert_cmpstr(text.get(), ==, "is a line");
+
+ g_assert_true(atspi_text_remove_selection(ATSPI_TEXT(p.get()), 0, nullptr));
+ selectionCount = atspi_text_get_n_selections(ATSPI_TEXT(p.get()), nullptr);
+ g_assert_cmpuint(selectionCount, ==, 0);
+ caretOffset = atspi_text_get_caret_offset(ATSPI_TEXT(p.get()), nullptr);
+ g_assert_cmpint(caretOffset, ==, 14);
+ g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(p.get()), 0, nullptr));
+ caretOffset = atspi_text_get_caret_offset(ATSPI_TEXT(p.get()), nullptr);
+ g_assert_cmpint(caretOffset, ==, 0);
+
+ auto section = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
+ g_assert_true(ATSPI_IS_ACCESSIBLE(section.get()));
+ g_assert_cmpint(atspi_accessible_get_child_count(section.get(), nullptr), ==, 1);
+ auto input = adoptGRef(atspi_accessible_get_child_at_index(section.get(), 0, nullptr));
+ g_assert_true(ATSPI_IS_TEXT(input.get()));
+ g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(input.get()), 5, nullptr));
+ caretOffset = atspi_text_get_caret_offset(ATSPI_TEXT(input.get()), nullptr);
+ g_assert_cmpint(caretOffset, ==, 5);
+ g_assert_true(atspi_text_set_selection(ATSPI_TEXT(input.get()), 0, 5, 12, nullptr));
+ selectionCount = atspi_text_get_n_selections(ATSPI_TEXT(input.get()), nullptr);
+ g_assert_cmpuint(selectionCount, ==, 1);
+ caretOffset = atspi_text_get_caret_offset(ATSPI_TEXT(input.get()), nullptr);
+ g_assert_cmpint(caretOffset, ==, 12);
+ selection.reset(atspi_text_get_selection(ATSPI_TEXT(input.get()), 0, nullptr));
+ g_assert_nonnull(selection.get());
+ g_assert_cmpint(selection->start_offset, ==, 5);
+ g_assert_cmpint(selection->end_offset, ==, 12);
+ text.reset(atspi_text_get_text(ATSPI_TEXT(input.get()), selection->start_offset, selection->end_offset, nullptr));
+ g_assert_cmpstr(text.get(), ==, "is text");
+}
+
+static void testTextAttributes(AccessibilityTest* test, gconstpointer)
+{
+ test->showInWindow(800, 600);
+ test->loadHtml(
+ "<html>"
+ " <body>"
+ " <p>This is a <b>line</b> of text</p>"
+ " </body>"
+ "</html>",
+ nullptr);
+ test->waitUntilLoadFinished();
+
+ auto testApp = test->findTestApplication();
+ g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));
+
+ auto documentWeb = test->findDocumentWeb(testApp.get());
+ g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
+ g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);
+
+ auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
+ g_assert_true(ATSPI_IS_TEXT(p.get()));
+
+ // Including default attributes.
+ int startOffset, endOffset;
+ GRefPtr<GHashTable> attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 0, TRUE, &startOffset, &endOffset, nullptr));
+ g_assert_nonnull(attributes.get());
+ g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 1);
+ g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "400");
+ g_assert_cmpint(startOffset, ==, 0);
+ g_assert_cmpint(endOffset, ==, 10);
+
+ attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 12, TRUE, &startOffset, &endOffset, nullptr));
+ g_assert_nonnull(attributes.get());
+ g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 1);
+ g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "700");
+ GUniquePtr<char> value(atspi_text_get_attribute_value(ATSPI_TEXT(p.get()), 11, const_cast<char*>("weight"), nullptr));
+ g_assert_cmpstr(value.get(), ==, "700");
+ g_assert_cmpint(startOffset, ==, 10);
+ g_assert_cmpint(endOffset, ==, 14);
+
+ attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 16, TRUE, &startOffset, &endOffset, nullptr));
+ g_assert_nonnull(attributes.get());
+ g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 1);
+ g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "400");
+ g_assert_cmpint(startOffset, ==, 14);
+ g_assert_cmpint(endOffset, ==, 22);
+
+ // Without default attributes.
+ attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 0, FALSE, &startOffset, &endOffset, nullptr));
+ g_assert_nonnull(attributes.get());
+ g_assert_cmpuint(g_hash_table_size(attributes.get()), ==, 0);
+ g_assert_cmpint(startOffset, ==, 0);
+ g_assert_cmpint(endOffset, ==, 10);
+
+ attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 12, FALSE, &startOffset, &endOffset, nullptr));
+ g_assert_nonnull(attributes.get());
+ g_assert_cmpuint(g_hash_table_size(attributes.get()), ==, 1);
+ g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "700");
+ g_assert_cmpint(startOffset, ==, 10);
+ g_assert_cmpint(endOffset, ==, 14);
+
+ attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 16, FALSE, &startOffset, &endOffset, nullptr));
+ g_assert_nonnull(attributes.get());
+ g_assert_cmpuint(g_hash_table_size(attributes.get()), ==, 0);
+ g_assert_cmpint(startOffset, ==, 14);
+ g_assert_cmpint(endOffset, ==, 22);
+
+ // Only default attributes.
+ attributes = adoptGRef(atspi_text_get_default_attributes(ATSPI_TEXT(p.get()), nullptr));
+ g_assert_nonnull(attributes.get());
+ g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 1);
+ g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "400");
+
+#if USE(ATSPI)
+ // Atspi implementation handles ranges with the same attributes as one run.
+ test->loadHtml(
+ "<html>"
+ " <body>"
+ " <p>This is a <b>li</b><b>ne</b> of text</p>"
+ " </body>"
+ "</html>",
+ nullptr);
+ test->waitUntilLoadFinished();
+
+ documentWeb = test->findDocumentWeb(testApp.get());
+ g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
+ g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);
+
+ p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
+ g_assert_true(ATSPI_IS_TEXT(p.get()));
+
+ attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 0, TRUE, &startOffset, &endOffset, nullptr));
+ g_assert_nonnull(attributes.get());
+ g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 1);
+ g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "400");
+ g_assert_cmpint(startOffset, ==, 0);
+ g_assert_cmpint(endOffset, ==, 10);
+
+ attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 12, TRUE, &startOffset, &endOffset, nullptr));
+ g_assert_nonnull(attributes.get());
+ g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 1);
+ g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "700");
+ g_assert_cmpint(startOffset, ==, 10);
+ g_assert_cmpint(endOffset, ==, 14);
+
+ attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 16, TRUE, &startOffset, &endOffset, nullptr));
+ g_assert_nonnull(attributes.get());
+ g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 1);
+ g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "400");
+ g_assert_cmpint(startOffset, ==, 14);
+ g_assert_cmpint(endOffset, ==, 22);
+
+#endif
+}
+
+static void testTextStateChanged(AccessibilityTest* test, gconstpointer)
+{
+ test->showInWindow(800, 600);
+ test->loadHtml(
+ "<html>"
+ " <body>"
+ " <p>This is a line of text</p>"
+ " <input value='This is a text field'/>"
+ " <div contenteditable=true>This is content editable</div>"
+ " </body>"
+ "</html>",
+ nullptr);
+ test->waitUntilLoadFinished();
+
+ auto testApp = test->findTestApplication();
+ g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));
+
+ auto documentWeb = test->findDocumentWeb(testApp.get());
+ g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
+ g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 3);
+
+ // Text caret moved.
+ auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
+ g_assert_true(ATSPI_IS_TEXT(p.get()));
+ test->startEventMonitor(p.get(), { "object:text-caret-moved" });
+ g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(p.get()), 10, nullptr));
+ auto events = test->stopEventMonitor(1);
+ g_assert_cmpuint(events.size(), ==, 1);
+ g_assert_cmpstr(events[0]->type, ==, "object:text-caret-moved");
+ g_assert_cmpuint(events[0]->detail1, ==, 10);
+
+ auto section = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
+ g_assert_true(ATSPI_IS_ACCESSIBLE(section.get()));
+ g_assert_cmpint(atspi_accessible_get_child_count(section.get(), nullptr), ==, 1);
+ auto input = adoptGRef(atspi_accessible_get_child_at_index(section.get(), 0, nullptr));
+ g_assert_true(ATSPI_IS_TEXT(input.get()));
+ test->startEventMonitor(input.get(), { "object:text-caret-moved" });
+ g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(input.get()), 5, nullptr));
+ events = test->stopEventMonitor(1);
+ g_assert_cmpuint(events.size(), ==, 1);
+ g_assert_cmpstr(events[0]->type, ==, "object:text-caret-moved");
+ g_assert_cmpuint(events[0]->detail1, ==, 5);
+
+ auto div = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 2, nullptr));
+ g_assert_true(ATSPI_IS_TEXT(div.get()));
+ test->startEventMonitor(div.get(), { "object:text-caret-moved" });
+ g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(div.get()), 15, nullptr));
+ events = test->stopEventMonitor(1);
+ g_assert_cmpuint(events.size(), ==, 1);
+ g_assert_cmpstr(events[0]->type, ==, "object:text-caret-moved");
+ g_assert_cmpuint(events[0]->detail1, ==, 15);
+
+#if USE(ATSPI) // Selection changed seems to be broken with ATK.
+ // Selection changed.
+ test->startEventMonitor(p.get(), { "object:text-selection-changed", "object:text-caret-moved" });
+ g_assert_true(atspi_text_set_selection(ATSPI_TEXT(p.get()), 0, 10, 15, nullptr));
+ events = test->stopEventMonitor(2);
+ g_assert_cmpuint(events.size(), ==, 2);
+ auto* event = AccessibilityTest::findEvent(events, "object:text-caret-moved");
+ g_assert_nonnull(event);
+ g_assert_cmpuint(event->detail1, ==, 15);
+ g_assert_nonnull(AccessibilityTest::findEvent(events, "object:text-selection-changed"));
+
+ test->startEventMonitor(input.get(), { "object:text-selection-changed", "object:text-caret-moved" });
+ g_assert_true(atspi_text_set_selection(ATSPI_TEXT(input.get()), 0, 5, 10, nullptr));
+ events = test->stopEventMonitor(2);
+ g_assert_cmpuint(events.size(), ==, 2);
+ event = AccessibilityTest::findEvent(events, "object:text-caret-moved");
+ g_assert_nonnull(event);
+ g_assert_cmpuint(event->detail1, ==, 10);
+ g_assert_nonnull(AccessibilityTest::findEvent(events, "object:text-selection-changed"));
+
+ test->startEventMonitor(div.get(), { "object:text-selection-changed", "object:text-caret-moved" });
+ g_assert_true(atspi_text_set_selection(ATSPI_TEXT(div.get()), 0, 15, 18, nullptr));
+ events = test->stopEventMonitor(2);
+ g_assert_cmpuint(events.size(), ==, 2);
+ event = AccessibilityTest::findEvent(events, "object:text-caret-moved");
+ g_assert_nonnull(event);
+ g_assert_cmpuint(event->detail1, ==, 18);
+ g_assert_nonnull(AccessibilityTest::findEvent(events, "object:text-selection-changed"));
+#endif
+
+ // Text changed.
+ g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(input.get()), 5, nullptr));
+ test->startEventMonitor(input.get(), { "object:text-changed:insert", "object:text-changed:delete" });
+ test->keyStroke(GDK_KEY_a);
+ events = test->stopEventMonitor(1);
+ g_assert_cmpuint(events.size(), ==, 1);
+ g_assert_cmpstr(events[0]->type, ==, "object:text-changed:insert");
+#if USE(ATSPI)
+ g_assert_cmpuint(events[0]->detail1, ==, 5);
+#endif
+ g_assert_cmpuint(events[0]->detail2, ==, 1);
+ g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
+ g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "a");
+ g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(input.get()), 0, nullptr));
+ test->startEventMonitor(input.get(), { "object:text-changed:insert", "object:text-changed:delete" });
+ test->keyStroke(GDK_KEY_Delete);
+ events = test->stopEventMonitor(1);
+ g_assert_cmpuint(events.size(), ==, 1);
+ g_assert_cmpstr(events[0]->type, ==, "object:text-changed:delete");
+ g_assert_cmpuint(events[0]->detail1, ==, 0);
+ g_assert_cmpuint(events[0]->detail2, ==, 1);
+ g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
+ g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "T");
+
+ g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(div.get()), 10, nullptr));
+ test->startEventMonitor(div.get(), { "object:text-changed:insert", "object:text-changed:delete" });
+ test->keyStroke(GDK_KEY_b);
+ events = test->stopEventMonitor(1);
+ g_assert_cmpuint(events.size(), ==, 1);
+ g_assert_cmpstr(events[0]->type, ==, "object:text-changed:insert");
+#if USE(ATSPI)
+ g_assert_cmpuint(events[0]->detail1, ==, 10);
+#endif
+ g_assert_cmpuint(events[0]->detail2, ==, 1);
+ g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
+ g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "b");
+ g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(div.get()), 15, nullptr));
+ test->startEventMonitor(div.get(), { "object:text-changed:insert", "object:text-changed:delete" });
+ test->keyStroke(GDK_KEY_BackSpace);
+ events = test->stopEventMonitor(1);
+ g_assert_cmpuint(events.size(), ==, 1);
+ g_assert_cmpstr(events[0]->type, ==, "object:text-changed:delete");
+#if USE(ATSPI)
+ g_assert_cmpuint(events[0]->detail1, ==, 14);
+#endif
+ g_assert_cmpuint(events[0]->detail2, ==, 1);
+ g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
+ g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "n");
+
+ // Text replaced.
+ g_assert_true(atspi_text_set_selection(ATSPI_TEXT(input.get()), 0, 5, 10, nullptr));
+ test->startEventMonitor(input.get(), { "object:text-changed:insert", "object:text-changed:delete" });
+ test->keyStroke(GDK_KEY_c);
+ events = test->stopEventMonitor(2);
+ g_assert_cmpuint(events.size(), ==, 2);
+ g_assert_cmpstr(events[0]->type, ==, "object:text-changed:delete");
+ g_assert_cmpuint(events[0]->detail1, ==, 6);
+ g_assert_cmpuint(events[0]->detail2, ==, 5);
+ g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
+ g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "is a ");
+ g_assert_cmpstr(events[1]->type, ==, "object:text-changed:insert");
+#if USE(ATSPI)
+ g_assert_cmpuint(events[1]->detail1, ==, 5);
+#endif
+ g_assert_cmpuint(events[1]->detail2, ==, 1);
+ g_assert_true(G_VALUE_HOLDS_STRING(&events[1]->any_data));
+ g_assert_cmpstr(g_value_get_string(&events[1]->any_data), ==, "c");
+
+ g_assert_true(atspi_text_set_selection(ATSPI_TEXT(div.get()), 0, 10, 18, nullptr));
+ test->startEventMonitor(div.get(), { "object:text-changed:insert", "object:text-changed:delete" });
+ test->keyStroke(GDK_KEY_Delete);
+ events = test->stopEventMonitor(1);
+ g_assert_cmpuint(events.size(), ==, 1);
+ g_assert_cmpstr(events[0]->type, ==, "object:text-changed:delete");
+#if USE(ATSPI)
+ g_assert_cmpuint(events[0]->detail1, ==, 10);
+#endif
+ g_assert_cmpuint(events[0]->detail2, ==, 8);
+ g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
+ g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "bntet ed");
+
+#if USE(ATSPI)
+ // Text input value changed.
+ test->startEventMonitor(input.get(), { "object:text-changed:insert", "object:text-changed:delete" });
+ test->runJavaScriptAndWaitUntilFinished("document.getElementsByTagName('input')[0].value = 'foo';", nullptr);
+ events = test->stopEventMonitor(2);
+ g_assert_cmpuint(events.size(), ==, 2);
+ g_assert_cmpstr(events[0]->type, ==, "object:text-changed:delete");
+ g_assert_cmpuint(events[0]->detail1, ==, 0);
+ g_assert_cmpuint(events[0]->detail2, ==, 16);
+ g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
+ g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "his actext field");
+ g_assert_cmpstr(events[1]->type, ==, "object:text-changed:insert");
+ g_assert_cmpuint(events[1]->detail1, ==, 0);
+ g_assert_cmpuint(events[1]->detail2, ==, 3);
+ g_assert_true(G_VALUE_HOLDS_STRING(&events[1]->any_data));
+ g_assert_cmpstr(g_value_get_string(&events[1]->any_data), ==, "foo");
+#endif
+}
+
void beforeAll()
{
AccessibilityTest::add("WebKitAccessibility", "accessible/basic-hierarchy", testAccessibleBasicHierarchy);
@@ -817,6 +1527,13 @@
#ifdef ATSPI_SCROLLTYPE_COUNT
AccessibilityTest::add("WebKitAccessibility", "component/scroll-to", testComponentScrollTo);
#endif
+ AccessibilityTest::add("WebKitAccessibility", "text/basic", testTextBasic);
+ AccessibilityTest::add("WebKitAccessibility", "text/surrogate-pair", testTextSurrogatePair);
+ AccessibilityTest::add("WebKitAccessibility", "text/iterator", testTextIterator);
+ AccessibilityTest::add("WebKitAccessibility", "text/extents", testTextExtents);
+ AccessibilityTest::add("WebKitAccessibility", "text/selections", testTextSelections);
+ AccessibilityTest::add("WebKitAccessibility", "text/attributes", testTextAttributes);
+ AccessibilityTest::add("WebKitAccessibility", "text/state-changed", testTextStateChanged);
}
void afterAll()