Skip to content

Commit

Permalink
[Linux][a11y] implement AtkText::get_text/string_at_offset() (flutter…
Browse files Browse the repository at this point in the history
…#38144)

This PR implements `AtkText::get_string_at_offset()` (and the deprecated `AtkText::get_text_at_offset()` still used by e.g. Orca) for `FlAccessibleTextField` to allow Orca to read out loud the current character while moving the text cursor around.

### Before (unmute to hear the screen reader)

[textfield-a11y-before.webm](https://user-images.githubusercontent.com/140617/206556644-fb4f4df8-acca-4d97-86d5-7120f0a4871d.webm)

### After (unmute to hear the screen reader)

[textfield-a11y-after.webm](https://user-images.githubusercontent.com/140617/206556678-4fbf9112-291e-4518-a258-e9ca33469430.webm)

Fixes: flutter/flutter#113049

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
  • Loading branch information
jpnurmi authored Jun 7, 2023
1 parent a6f7c77 commit 671a846
Show file tree
Hide file tree
Showing 6 changed files with 366 additions and 1 deletion.
15 changes: 15 additions & 0 deletions shell/platform/linux/fl_accessible_node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,11 @@ static void fl_accessible_node_set_text_selection_impl(FlAccessibleNode* self,
gint base,
gint extent) {}

// Implements FlAccessibleNode::set_text_direction.
static void fl_accessible_node_set_text_direction_impl(
FlAccessibleNode* self,
FlutterTextDirection direction) {}

// Implements FlAccessibleNode::perform_action.
static void fl_accessible_node_perform_action_impl(
FlAccessibleNode* self,
Expand Down Expand Up @@ -436,6 +441,8 @@ static void fl_accessible_node_class_init(FlAccessibleNodeClass* klass) {
fl_accessible_node_set_value_impl;
FL_ACCESSIBLE_NODE_CLASS(klass)->set_text_selection =
fl_accessible_node_set_text_selection_impl;
FL_ACCESSIBLE_NODE_CLASS(klass)->set_text_direction =
fl_accessible_node_set_text_direction_impl;
FL_ACCESSIBLE_NODE_CLASS(klass)->perform_action =
fl_accessible_node_perform_action_impl;

Expand Down Expand Up @@ -561,6 +568,14 @@ void fl_accessible_node_set_text_selection(FlAccessibleNode* self,
extent);
}

void fl_accessible_node_set_text_direction(FlAccessibleNode* self,
FlutterTextDirection direction) {
g_return_if_fail(FL_IS_ACCESSIBLE_NODE(self));

return FL_ACCESSIBLE_NODE_GET_CLASS(self)->set_text_direction(self,
direction);
}

void fl_accessible_node_perform_action(FlAccessibleNode* self,
FlutterSemanticsAction action,
GBytes* data) {
Expand Down
12 changes: 12 additions & 0 deletions shell/platform/linux/fl_accessible_node.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ struct _FlAccessibleNodeClass {
void (*set_actions)(FlAccessibleNode* node, FlutterSemanticsAction actions);
void (*set_value)(FlAccessibleNode* node, const gchar* value);
void (*set_text_selection)(FlAccessibleNode* node, gint base, gint extent);
void (*set_text_direction)(FlAccessibleNode* node,
FlutterTextDirection direction);

void (*perform_action)(FlAccessibleNode* node,
FlutterSemanticsAction action,
Expand Down Expand Up @@ -151,6 +153,16 @@ void fl_accessible_node_set_text_selection(FlAccessibleNode* node,
gint base,
gint extent);

/**
* fl_accessible_node_set_text_direction:
* @node: an #FlAccessibleNode.
* @direction: the direction of the text.
*
* Sets the text direction of this node.
*/
void fl_accessible_node_set_text_direction(FlAccessibleNode* node,
FlutterTextDirection direction);

/**
* fl_accessible_node_dispatch_action:
* @node: an #FlAccessibleNode.
Expand Down
219 changes: 218 additions & 1 deletion shell/platform/linux/fl_accessible_text_field.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@
#include "flutter/shell/platform/linux/public/flutter_linux/fl_standard_message_codec.h"
#include "flutter/shell/platform/linux/public/flutter_linux/fl_value.h"

G_DEFINE_AUTOPTR_CLEANUP_FUNC(PangoContext, g_object_unref)
G_DEFINE_AUTOPTR_CLEANUP_FUNC(PangoLayout, g_object_unref)

typedef bool (*FlTextBoundaryCallback)(const PangoLogAttr* attr);

struct _FlAccessibleTextField {
FlAccessibleNode parent_instance;

gint selection_base;
gint selection_extent;
GtkEntryBuffer* buffer;
FlutterTextDirection text_direction;
};

static void fl_accessible_text_iface_init(AtkTextIface* iface);
Expand All @@ -36,6 +42,145 @@ static gchar* get_substring(FlAccessibleTextField* self,
return g_utf8_substring(value, start, end);
}

static PangoContext* get_pango_context(FlAccessibleTextField* self) {
PangoFontMap* font_map = pango_cairo_font_map_get_default();
PangoContext* context = pango_font_map_create_context(font_map);
pango_context_set_base_dir(context,
self->text_direction == kFlutterTextDirectionRTL
? PANGO_DIRECTION_RTL
: PANGO_DIRECTION_LTR);
return context;
}

static PangoLayout* create_pango_layout(FlAccessibleTextField* self) {
g_autoptr(PangoContext) context = get_pango_context(self);
PangoLayout* layout = pango_layout_new(context);
pango_layout_set_text(layout, gtk_entry_buffer_get_text(self->buffer), -1);
return layout;
}

static gchar* get_string_at_offset(FlAccessibleTextField* self,
gint start,
gint end,
FlTextBoundaryCallback is_start,
FlTextBoundaryCallback is_end,
gint* start_offset,
gint* end_offset) {
g_autoptr(PangoLayout) layout = create_pango_layout(self);

gint n_attrs = 0;
const PangoLogAttr* attrs =
pango_layout_get_log_attrs_readonly(layout, &n_attrs);

while (start > 0 && !is_start(&attrs[start])) {
--start;
}
if (start_offset != nullptr) {
*start_offset = start;
}

while (end < n_attrs && !is_end(&attrs[end])) {
++end;
}
if (end_offset != nullptr) {
*end_offset = end;
}

return get_substring(self, start, end);
}

static gchar* get_char_at_offset(FlAccessibleTextField* self,
gint offset,
gint* start_offset,
gint* end_offset) {
return get_string_at_offset(
self, offset, offset + 1,
[](const PangoLogAttr* attr) -> bool { return attr->is_char_break; },
[](const PangoLogAttr* attr) -> bool { return attr->is_char_break; },
start_offset, end_offset);
}

static gchar* get_word_at_offset(FlAccessibleTextField* self,
gint offset,
gint* start_offset,
gint* end_offset) {
return get_string_at_offset(
self, offset, offset,
[](const PangoLogAttr* attr) -> bool { return attr->is_word_start; },
[](const PangoLogAttr* attr) -> bool { return attr->is_word_end; },
start_offset, end_offset);
}

static gchar* get_sentence_at_offset(FlAccessibleTextField* self,
gint offset,
gint* start_offset,
gint* end_offset) {
return get_string_at_offset(
self, offset, offset,
[](const PangoLogAttr* attr) -> bool { return attr->is_sentence_start; },
[](const PangoLogAttr* attr) -> bool { return attr->is_sentence_end; },
start_offset, end_offset);
}

static gchar* get_line_at_offset(FlAccessibleTextField* self,
gint offset,
gint* start_offset,
gint* end_offset) {
g_autoptr(PangoLayout) layout = create_pango_layout(self);

GSList* lines = pango_layout_get_lines_readonly(layout);
while (lines != nullptr) {
PangoLayoutLine* line = static_cast<PangoLayoutLine*>(lines->data);
if (offset >= line->start_index &&
offset <= line->start_index + line->length) {
if (start_offset != nullptr) {
*start_offset = line->start_index;
}
if (end_offset != nullptr) {
*end_offset = line->start_index + line->length;
}
return get_substring(self, line->start_index,
line->start_index + line->length);
}
lines = lines->next;
}

return nullptr;
}

static gchar* get_paragraph_at_offset(FlAccessibleTextField* self,
gint offset,
gint* start_offset,
gint* end_offset) {
g_autoptr(PangoLayout) layout = create_pango_layout(self);

PangoLayoutLine* start = nullptr;
PangoLayoutLine* end = nullptr;
gint n_lines = pango_layout_get_line_count(layout);
for (gint i = 0; i < n_lines; ++i) {
PangoLayoutLine* line = pango_layout_get_line(layout, i);
if (line->is_paragraph_start) {
end = line;
}
if (start != nullptr && end != nullptr && offset >= start->start_index &&
offset <= end->start_index + end->length) {
if (start_offset != nullptr) {
*start_offset = start->start_index;
}
if (end_offset != nullptr) {
*end_offset = end->start_index + end->length;
}
return get_substring(self, start->start_index,
end->start_index + end->length);
}
if (line->is_paragraph_start) {
start = line;
}
}

return nullptr;
}

static void perform_set_text_action(FlAccessibleTextField* self,
const char* text) {
g_autoptr(FlValue) value = fl_value_new_string(text);
Expand Down Expand Up @@ -109,6 +254,16 @@ static void fl_accessible_text_field_set_text_selection(FlAccessibleNode* node,
}
}

// Implements FlAccessibleNode::set_text_direction.
static void fl_accessible_text_field_set_text_direction(
FlAccessibleNode* node,
FlutterTextDirection direction) {
g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(node));
FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(node);

self->text_direction = direction;
}

// Overrides FlAccessibleNode::perform_action.
void fl_accessible_text_field_perform_action(FlAccessibleNode* self,
FlutterSemanticsAction action,
Expand Down Expand Up @@ -154,6 +309,65 @@ static gchar* fl_accessible_text_field_get_text(AtkText* text,
return get_substring(self, start_offset, end_offset);
}

// Implements AtkText::get_string_at_offset.
static gchar* fl_accessible_text_field_get_string_at_offset(
AtkText* text,
gint offset,
AtkTextGranularity granularity,
gint* start_offset,
gint* end_offset) {
g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), nullptr);
FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text);

switch (granularity) {
case ATK_TEXT_GRANULARITY_CHAR:
return get_char_at_offset(self, offset, start_offset, end_offset);
case ATK_TEXT_GRANULARITY_WORD:
return get_word_at_offset(self, offset, start_offset, end_offset);
case ATK_TEXT_GRANULARITY_SENTENCE:
return get_sentence_at_offset(self, offset, start_offset, end_offset);
case ATK_TEXT_GRANULARITY_LINE:
return get_line_at_offset(self, offset, start_offset, end_offset);
case ATK_TEXT_GRANULARITY_PARAGRAPH:
return get_paragraph_at_offset(self, offset, start_offset, end_offset);
default:
return nullptr;
}
}

// Implements AtkText::get_text_at_offset (deprecated but still commonly used).
static gchar* fl_accessible_text_field_get_text_at_offset(
AtkText* text,
gint offset,
AtkTextBoundary boundary_type,
gint* start_offset,
gint* end_offset) {
switch (boundary_type) {
case ATK_TEXT_BOUNDARY_CHAR:
return fl_accessible_text_field_get_string_at_offset(
text, offset, ATK_TEXT_GRANULARITY_CHAR, start_offset, end_offset);
break;
case ATK_TEXT_BOUNDARY_WORD_START:
case ATK_TEXT_BOUNDARY_WORD_END:
return fl_accessible_text_field_get_string_at_offset(
text, offset, ATK_TEXT_GRANULARITY_WORD, start_offset, end_offset);
break;
case ATK_TEXT_BOUNDARY_SENTENCE_START:
case ATK_TEXT_BOUNDARY_SENTENCE_END:
return fl_accessible_text_field_get_string_at_offset(
text, offset, ATK_TEXT_GRANULARITY_SENTENCE, start_offset,
end_offset);
break;
case ATK_TEXT_BOUNDARY_LINE_START:
case ATK_TEXT_BOUNDARY_LINE_END:
return fl_accessible_text_field_get_string_at_offset(
text, offset, ATK_TEXT_GRANULARITY_LINE, start_offset, end_offset);
break;
default:
return nullptr;
}
}

// Implements AtkText::get_caret_offset.
static gint fl_accessible_text_field_get_caret_offset(AtkText* text) {
g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), -1);
Expand Down Expand Up @@ -338,14 +552,17 @@ static void fl_accessible_text_field_class_init(
fl_accessible_text_field_set_value;
FL_ACCESSIBLE_NODE_CLASS(klass)->set_text_selection =
fl_accessible_text_field_set_text_selection;
FL_ACCESSIBLE_NODE_CLASS(klass)->set_text_direction =
fl_accessible_text_field_set_text_direction;
FL_ACCESSIBLE_NODE_CLASS(klass)->perform_action =
fl_accessible_text_field_perform_action;
}

static void fl_accessible_text_iface_init(AtkTextIface* iface) {
iface->get_character_count = fl_accessible_text_field_get_character_count;
iface->get_text = fl_accessible_text_field_get_text;
// TODO(jpnurmi): get_text_at/before/after_offset
iface->get_text_at_offset = fl_accessible_text_field_get_text_at_offset;
iface->get_string_at_offset = fl_accessible_text_field_get_string_at_offset;

iface->get_caret_offset = fl_accessible_text_field_get_caret_offset;
iface->set_caret_offset = fl_accessible_text_field_set_caret_offset;
Expand Down
Loading

0 comments on commit 671a846

Please sign in to comment.