Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 671a846

Browse files
authored
[Linux][a11y] implement AtkText::get_text/string_at_offset() (#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
1 parent a6f7c77 commit 671a846

File tree

6 files changed

+366
-1
lines changed

6 files changed

+366
-1
lines changed

shell/platform/linux/fl_accessible_node.cc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,11 @@ static void fl_accessible_node_set_text_selection_impl(FlAccessibleNode* self,
405405
gint base,
406406
gint extent) {}
407407

408+
// Implements FlAccessibleNode::set_text_direction.
409+
static void fl_accessible_node_set_text_direction_impl(
410+
FlAccessibleNode* self,
411+
FlutterTextDirection direction) {}
412+
408413
// Implements FlAccessibleNode::perform_action.
409414
static void fl_accessible_node_perform_action_impl(
410415
FlAccessibleNode* self,
@@ -436,6 +441,8 @@ static void fl_accessible_node_class_init(FlAccessibleNodeClass* klass) {
436441
fl_accessible_node_set_value_impl;
437442
FL_ACCESSIBLE_NODE_CLASS(klass)->set_text_selection =
438443
fl_accessible_node_set_text_selection_impl;
444+
FL_ACCESSIBLE_NODE_CLASS(klass)->set_text_direction =
445+
fl_accessible_node_set_text_direction_impl;
439446
FL_ACCESSIBLE_NODE_CLASS(klass)->perform_action =
440447
fl_accessible_node_perform_action_impl;
441448

@@ -561,6 +568,14 @@ void fl_accessible_node_set_text_selection(FlAccessibleNode* self,
561568
extent);
562569
}
563570

571+
void fl_accessible_node_set_text_direction(FlAccessibleNode* self,
572+
FlutterTextDirection direction) {
573+
g_return_if_fail(FL_IS_ACCESSIBLE_NODE(self));
574+
575+
return FL_ACCESSIBLE_NODE_GET_CLASS(self)->set_text_direction(self,
576+
direction);
577+
}
578+
564579
void fl_accessible_node_perform_action(FlAccessibleNode* self,
565580
FlutterSemanticsAction action,
566581
GBytes* data) {

shell/platform/linux/fl_accessible_node.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ struct _FlAccessibleNodeClass {
4545
void (*set_actions)(FlAccessibleNode* node, FlutterSemanticsAction actions);
4646
void (*set_value)(FlAccessibleNode* node, const gchar* value);
4747
void (*set_text_selection)(FlAccessibleNode* node, gint base, gint extent);
48+
void (*set_text_direction)(FlAccessibleNode* node,
49+
FlutterTextDirection direction);
4850

4951
void (*perform_action)(FlAccessibleNode* node,
5052
FlutterSemanticsAction action,
@@ -151,6 +153,16 @@ void fl_accessible_node_set_text_selection(FlAccessibleNode* node,
151153
gint base,
152154
gint extent);
153155

156+
/**
157+
* fl_accessible_node_set_text_direction:
158+
* @node: an #FlAccessibleNode.
159+
* @direction: the direction of the text.
160+
*
161+
* Sets the text direction of this node.
162+
*/
163+
void fl_accessible_node_set_text_direction(FlAccessibleNode* node,
164+
FlutterTextDirection direction);
165+
154166
/**
155167
* fl_accessible_node_dispatch_action:
156168
* @node: an #FlAccessibleNode.

shell/platform/linux/fl_accessible_text_field.cc

Lines changed: 218 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,18 @@
66
#include "flutter/shell/platform/linux/public/flutter_linux/fl_standard_message_codec.h"
77
#include "flutter/shell/platform/linux/public/flutter_linux/fl_value.h"
88

9+
G_DEFINE_AUTOPTR_CLEANUP_FUNC(PangoContext, g_object_unref)
10+
G_DEFINE_AUTOPTR_CLEANUP_FUNC(PangoLayout, g_object_unref)
11+
12+
typedef bool (*FlTextBoundaryCallback)(const PangoLogAttr* attr);
13+
914
struct _FlAccessibleTextField {
1015
FlAccessibleNode parent_instance;
1116

1217
gint selection_base;
1318
gint selection_extent;
1419
GtkEntryBuffer* buffer;
20+
FlutterTextDirection text_direction;
1521
};
1622

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

45+
static PangoContext* get_pango_context(FlAccessibleTextField* self) {
46+
PangoFontMap* font_map = pango_cairo_font_map_get_default();
47+
PangoContext* context = pango_font_map_create_context(font_map);
48+
pango_context_set_base_dir(context,
49+
self->text_direction == kFlutterTextDirectionRTL
50+
? PANGO_DIRECTION_RTL
51+
: PANGO_DIRECTION_LTR);
52+
return context;
53+
}
54+
55+
static PangoLayout* create_pango_layout(FlAccessibleTextField* self) {
56+
g_autoptr(PangoContext) context = get_pango_context(self);
57+
PangoLayout* layout = pango_layout_new(context);
58+
pango_layout_set_text(layout, gtk_entry_buffer_get_text(self->buffer), -1);
59+
return layout;
60+
}
61+
62+
static gchar* get_string_at_offset(FlAccessibleTextField* self,
63+
gint start,
64+
gint end,
65+
FlTextBoundaryCallback is_start,
66+
FlTextBoundaryCallback is_end,
67+
gint* start_offset,
68+
gint* end_offset) {
69+
g_autoptr(PangoLayout) layout = create_pango_layout(self);
70+
71+
gint n_attrs = 0;
72+
const PangoLogAttr* attrs =
73+
pango_layout_get_log_attrs_readonly(layout, &n_attrs);
74+
75+
while (start > 0 && !is_start(&attrs[start])) {
76+
--start;
77+
}
78+
if (start_offset != nullptr) {
79+
*start_offset = start;
80+
}
81+
82+
while (end < n_attrs && !is_end(&attrs[end])) {
83+
++end;
84+
}
85+
if (end_offset != nullptr) {
86+
*end_offset = end;
87+
}
88+
89+
return get_substring(self, start, end);
90+
}
91+
92+
static gchar* get_char_at_offset(FlAccessibleTextField* self,
93+
gint offset,
94+
gint* start_offset,
95+
gint* end_offset) {
96+
return get_string_at_offset(
97+
self, offset, offset + 1,
98+
[](const PangoLogAttr* attr) -> bool { return attr->is_char_break; },
99+
[](const PangoLogAttr* attr) -> bool { return attr->is_char_break; },
100+
start_offset, end_offset);
101+
}
102+
103+
static gchar* get_word_at_offset(FlAccessibleTextField* self,
104+
gint offset,
105+
gint* start_offset,
106+
gint* end_offset) {
107+
return get_string_at_offset(
108+
self, offset, offset,
109+
[](const PangoLogAttr* attr) -> bool { return attr->is_word_start; },
110+
[](const PangoLogAttr* attr) -> bool { return attr->is_word_end; },
111+
start_offset, end_offset);
112+
}
113+
114+
static gchar* get_sentence_at_offset(FlAccessibleTextField* self,
115+
gint offset,
116+
gint* start_offset,
117+
gint* end_offset) {
118+
return get_string_at_offset(
119+
self, offset, offset,
120+
[](const PangoLogAttr* attr) -> bool { return attr->is_sentence_start; },
121+
[](const PangoLogAttr* attr) -> bool { return attr->is_sentence_end; },
122+
start_offset, end_offset);
123+
}
124+
125+
static gchar* get_line_at_offset(FlAccessibleTextField* self,
126+
gint offset,
127+
gint* start_offset,
128+
gint* end_offset) {
129+
g_autoptr(PangoLayout) layout = create_pango_layout(self);
130+
131+
GSList* lines = pango_layout_get_lines_readonly(layout);
132+
while (lines != nullptr) {
133+
PangoLayoutLine* line = static_cast<PangoLayoutLine*>(lines->data);
134+
if (offset >= line->start_index &&
135+
offset <= line->start_index + line->length) {
136+
if (start_offset != nullptr) {
137+
*start_offset = line->start_index;
138+
}
139+
if (end_offset != nullptr) {
140+
*end_offset = line->start_index + line->length;
141+
}
142+
return get_substring(self, line->start_index,
143+
line->start_index + line->length);
144+
}
145+
lines = lines->next;
146+
}
147+
148+
return nullptr;
149+
}
150+
151+
static gchar* get_paragraph_at_offset(FlAccessibleTextField* self,
152+
gint offset,
153+
gint* start_offset,
154+
gint* end_offset) {
155+
g_autoptr(PangoLayout) layout = create_pango_layout(self);
156+
157+
PangoLayoutLine* start = nullptr;
158+
PangoLayoutLine* end = nullptr;
159+
gint n_lines = pango_layout_get_line_count(layout);
160+
for (gint i = 0; i < n_lines; ++i) {
161+
PangoLayoutLine* line = pango_layout_get_line(layout, i);
162+
if (line->is_paragraph_start) {
163+
end = line;
164+
}
165+
if (start != nullptr && end != nullptr && offset >= start->start_index &&
166+
offset <= end->start_index + end->length) {
167+
if (start_offset != nullptr) {
168+
*start_offset = start->start_index;
169+
}
170+
if (end_offset != nullptr) {
171+
*end_offset = end->start_index + end->length;
172+
}
173+
return get_substring(self, start->start_index,
174+
end->start_index + end->length);
175+
}
176+
if (line->is_paragraph_start) {
177+
start = line;
178+
}
179+
}
180+
181+
return nullptr;
182+
}
183+
39184
static void perform_set_text_action(FlAccessibleTextField* self,
40185
const char* text) {
41186
g_autoptr(FlValue) value = fl_value_new_string(text);
@@ -109,6 +254,16 @@ static void fl_accessible_text_field_set_text_selection(FlAccessibleNode* node,
109254
}
110255
}
111256

257+
// Implements FlAccessibleNode::set_text_direction.
258+
static void fl_accessible_text_field_set_text_direction(
259+
FlAccessibleNode* node,
260+
FlutterTextDirection direction) {
261+
g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(node));
262+
FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(node);
263+
264+
self->text_direction = direction;
265+
}
266+
112267
// Overrides FlAccessibleNode::perform_action.
113268
void fl_accessible_text_field_perform_action(FlAccessibleNode* self,
114269
FlutterSemanticsAction action,
@@ -154,6 +309,65 @@ static gchar* fl_accessible_text_field_get_text(AtkText* text,
154309
return get_substring(self, start_offset, end_offset);
155310
}
156311

312+
// Implements AtkText::get_string_at_offset.
313+
static gchar* fl_accessible_text_field_get_string_at_offset(
314+
AtkText* text,
315+
gint offset,
316+
AtkTextGranularity granularity,
317+
gint* start_offset,
318+
gint* end_offset) {
319+
g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), nullptr);
320+
FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text);
321+
322+
switch (granularity) {
323+
case ATK_TEXT_GRANULARITY_CHAR:
324+
return get_char_at_offset(self, offset, start_offset, end_offset);
325+
case ATK_TEXT_GRANULARITY_WORD:
326+
return get_word_at_offset(self, offset, start_offset, end_offset);
327+
case ATK_TEXT_GRANULARITY_SENTENCE:
328+
return get_sentence_at_offset(self, offset, start_offset, end_offset);
329+
case ATK_TEXT_GRANULARITY_LINE:
330+
return get_line_at_offset(self, offset, start_offset, end_offset);
331+
case ATK_TEXT_GRANULARITY_PARAGRAPH:
332+
return get_paragraph_at_offset(self, offset, start_offset, end_offset);
333+
default:
334+
return nullptr;
335+
}
336+
}
337+
338+
// Implements AtkText::get_text_at_offset (deprecated but still commonly used).
339+
static gchar* fl_accessible_text_field_get_text_at_offset(
340+
AtkText* text,
341+
gint offset,
342+
AtkTextBoundary boundary_type,
343+
gint* start_offset,
344+
gint* end_offset) {
345+
switch (boundary_type) {
346+
case ATK_TEXT_BOUNDARY_CHAR:
347+
return fl_accessible_text_field_get_string_at_offset(
348+
text, offset, ATK_TEXT_GRANULARITY_CHAR, start_offset, end_offset);
349+
break;
350+
case ATK_TEXT_BOUNDARY_WORD_START:
351+
case ATK_TEXT_BOUNDARY_WORD_END:
352+
return fl_accessible_text_field_get_string_at_offset(
353+
text, offset, ATK_TEXT_GRANULARITY_WORD, start_offset, end_offset);
354+
break;
355+
case ATK_TEXT_BOUNDARY_SENTENCE_START:
356+
case ATK_TEXT_BOUNDARY_SENTENCE_END:
357+
return fl_accessible_text_field_get_string_at_offset(
358+
text, offset, ATK_TEXT_GRANULARITY_SENTENCE, start_offset,
359+
end_offset);
360+
break;
361+
case ATK_TEXT_BOUNDARY_LINE_START:
362+
case ATK_TEXT_BOUNDARY_LINE_END:
363+
return fl_accessible_text_field_get_string_at_offset(
364+
text, offset, ATK_TEXT_GRANULARITY_LINE, start_offset, end_offset);
365+
break;
366+
default:
367+
return nullptr;
368+
}
369+
}
370+
157371
// Implements AtkText::get_caret_offset.
158372
static gint fl_accessible_text_field_get_caret_offset(AtkText* text) {
159373
g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), -1);
@@ -338,14 +552,17 @@ static void fl_accessible_text_field_class_init(
338552
fl_accessible_text_field_set_value;
339553
FL_ACCESSIBLE_NODE_CLASS(klass)->set_text_selection =
340554
fl_accessible_text_field_set_text_selection;
555+
FL_ACCESSIBLE_NODE_CLASS(klass)->set_text_direction =
556+
fl_accessible_text_field_set_text_direction;
341557
FL_ACCESSIBLE_NODE_CLASS(klass)->perform_action =
342558
fl_accessible_text_field_perform_action;
343559
}
344560

345561
static void fl_accessible_text_iface_init(AtkTextIface* iface) {
346562
iface->get_character_count = fl_accessible_text_field_get_character_count;
347563
iface->get_text = fl_accessible_text_field_get_text;
348-
// TODO(jpnurmi): get_text_at/before/after_offset
564+
iface->get_text_at_offset = fl_accessible_text_field_get_text_at_offset;
565+
iface->get_string_at_offset = fl_accessible_text_field_get_string_at_offset;
349566

350567
iface->get_caret_offset = fl_accessible_text_field_get_caret_offset;
351568
iface->set_caret_offset = fl_accessible_text_field_set_caret_offset;

0 commit comments

Comments
 (0)