blob: 9dcc7f64fb82048ca0e7232ae830c3be8729ff4d [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "flutter/shell/platform/linux/fl_accessible_text_field.h"
#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)
// PangoLayout g_autoptr macro weren't added until 1.49.4. Add them manually.
// https://gitlab.gnome.org/GNOME/pango/-/commit/0b84e14
#if !PANGO_VERSION_CHECK(1, 49, 4)
G_DEFINE_AUTOPTR_CLEANUP_FUNC(PangoLayout, g_object_unref)
#endif
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);
static void fl_accessible_editable_text_iface_init(AtkEditableTextIface* iface);
G_DEFINE_TYPE_WITH_CODE(
FlAccessibleTextField,
fl_accessible_text_field,
FL_TYPE_ACCESSIBLE_NODE,
G_IMPLEMENT_INTERFACE(ATK_TYPE_TEXT, fl_accessible_text_iface_init)
G_IMPLEMENT_INTERFACE(ATK_TYPE_EDITABLE_TEXT,
fl_accessible_editable_text_iface_init))
static gchar* get_substring(FlAccessibleTextField* self,
glong start,
glong end) {
const gchar* value = gtk_entry_buffer_get_text(self->buffer);
if (end == -1) {
// g_utf8_substring() accepts -1 since 2.72
end = g_utf8_strlen(value, -1);
}
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);
g_autoptr(FlStandardMessageCodec) codec = fl_standard_message_codec_new();
g_autoptr(GBytes) message =
fl_message_codec_encode_message(FL_MESSAGE_CODEC(codec), value, nullptr);
fl_accessible_node_perform_action(FL_ACCESSIBLE_NODE(self),
kFlutterSemanticsActionSetText, message);
}
static void perform_set_selection_action(FlAccessibleTextField* self,
gint base,
gint extent) {
g_autoptr(FlValue) value = fl_value_new_map();
fl_value_set_string_take(value, "base", fl_value_new_int(base));
fl_value_set_string_take(value, "extent", fl_value_new_int(extent));
g_autoptr(FlStandardMessageCodec) codec = fl_standard_message_codec_new();
g_autoptr(GBytes) message =
fl_message_codec_encode_message(FL_MESSAGE_CODEC(codec), value, nullptr);
fl_accessible_node_perform_action(
FL_ACCESSIBLE_NODE(self), kFlutterSemanticsActionSetSelection, message);
}
// Implements GObject::dispose.
static void fl_accessible_text_field_dispose(GObject* object) {
FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(object);
g_clear_object(&self->buffer);
G_OBJECT_CLASS(fl_accessible_text_field_parent_class)->dispose(object);
}
// Implements FlAccessibleNode::set_value.
static void fl_accessible_text_field_set_value(FlAccessibleNode* node,
const gchar* value) {
g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(node));
FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(node);
if (g_strcmp0(gtk_entry_buffer_get_text(self->buffer), value) == 0) {
return;
}
gtk_entry_buffer_set_text(self->buffer, value, -1);
}
// Implements FlAccessibleNode::set_text_selection.
static void fl_accessible_text_field_set_text_selection(FlAccessibleNode* node,
gint base,
gint extent) {
g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(node));
FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(node);
gboolean caret_moved = extent != self->selection_extent;
gboolean has_selection = base != extent;
gboolean had_selection = self->selection_base != self->selection_extent;
gboolean selection_changed = (has_selection || had_selection) &&
(caret_moved || base != self->selection_base);
self->selection_base = base;
self->selection_extent = extent;
if (selection_changed) {
g_signal_emit_by_name(self, "text-selection-changed", nullptr);
}
if (caret_moved) {
g_signal_emit_by_name(self, "text-caret-moved", extent, nullptr);
}
}
// 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,
GBytes* data) {
FlAccessibleNodeClass* parent_class =
FL_ACCESSIBLE_NODE_CLASS(fl_accessible_text_field_parent_class);
switch (action) {
case kFlutterSemanticsActionMoveCursorForwardByCharacter:
case kFlutterSemanticsActionMoveCursorBackwardByCharacter:
case kFlutterSemanticsActionMoveCursorForwardByWord:
case kFlutterSemanticsActionMoveCursorBackwardByWord: {
// These actions require a boolean argument that indicates whether the
// selection should be extended or collapsed when moving the cursor.
g_autoptr(FlValue) extend_selection = fl_value_new_bool(false);
g_autoptr(FlStandardMessageCodec) codec = fl_standard_message_codec_new();
g_autoptr(GBytes) message = fl_message_codec_encode_message(
FL_MESSAGE_CODEC(codec), extend_selection, nullptr);
parent_class->perform_action(self, action, message);
break;
}
default:
parent_class->perform_action(self, action, data);
break;
}
}
// Implements AtkText::get_character_count.
static gint fl_accessible_text_field_get_character_count(AtkText* text) {
g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), 0);
FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text);
return gtk_entry_buffer_get_length(self->buffer);
}
// Implements AtkText::get_text.
static gchar* fl_accessible_text_field_get_text(AtkText* text,
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);
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);
FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text);
return self->selection_extent;
}
// Implements AtkText::set_caret_offset.
static gboolean fl_accessible_text_field_set_caret_offset(AtkText* text,
gint offset) {
g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), false);
FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text);
perform_set_selection_action(self, offset, offset);
return true;
}
// Implements AtkText::get_n_selections.
static gint fl_accessible_text_field_get_n_selections(AtkText* text) {
g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), 0);
FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text);
if (self->selection_base == self->selection_extent) {
return 0;
}
return 1;
}
// Implements AtkText::get_selection.
static gchar* fl_accessible_text_field_get_selection(AtkText* text,
gint selection_num,
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);
if (selection_num != 0 || self->selection_base == self->selection_extent) {
return nullptr;
}
gint start = MIN(self->selection_base, self->selection_extent);
gint end = MAX(self->selection_base, self->selection_extent);
if (start_offset != nullptr) {
*start_offset = start;
}
if (end_offset != nullptr) {
*end_offset = end;
}
return get_substring(self, start, end);
}
// Implements AtkText::add_selection.
static gboolean fl_accessible_text_field_add_selection(AtkText* text,
gint start_offset,
gint end_offset) {
g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), false);
FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text);
if (self->selection_base != self->selection_extent) {
return false;
}
perform_set_selection_action(self, start_offset, end_offset);
return true;
}
// Implements AtkText::remove_selection.
static gboolean fl_accessible_text_field_remove_selection(AtkText* text,
gint selection_num) {
g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), false);
FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text);
if (selection_num != 0 || self->selection_base == self->selection_extent) {
return false;
}
perform_set_selection_action(self, self->selection_extent,
self->selection_extent);
return true;
}
// Implements AtkText::set_selection.
static gboolean fl_accessible_text_field_set_selection(AtkText* text,
gint selection_num,
gint start_offset,
gint end_offset) {
g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), false);
FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text);
if (selection_num != 0) {
return false;
}
perform_set_selection_action(self, start_offset, end_offset);
return true;
}
// Implements AtkEditableText::set_text_contents.
static void fl_accessible_text_field_set_text_contents(
AtkEditableText* editable_text,
const gchar* string) {
g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(editable_text));
FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(editable_text);
perform_set_text_action(self, string);
}
// Implements AtkEditableText::insert_text.
static void fl_accessible_text_field_insert_text(AtkEditableText* editable_text,
const gchar* string,
gint length,
gint* position) {
g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(editable_text));
FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(editable_text);
*position +=
gtk_entry_buffer_insert_text(self->buffer, *position, string, length);
perform_set_text_action(self, gtk_entry_buffer_get_text(self->buffer));
perform_set_selection_action(self, *position, *position);
}
// Implements AtkEditableText::delete_text.
static void fl_accessible_node_delete_text(AtkEditableText* editable_text,
gint start_pos,
gint end_pos) {
g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(editable_text));
FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(editable_text);
gtk_entry_buffer_delete_text(self->buffer, start_pos, end_pos - start_pos);
perform_set_text_action(self, gtk_entry_buffer_get_text(self->buffer));
perform_set_selection_action(self, start_pos, start_pos);
}
// Implement AtkEditableText::copy_text.
static void fl_accessible_text_field_copy_text(AtkEditableText* editable_text,
gint start_pos,
gint end_pos) {
g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(editable_text));
FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(editable_text);
perform_set_selection_action(self, start_pos, end_pos);
fl_accessible_node_perform_action(FL_ACCESSIBLE_NODE(editable_text),
kFlutterSemanticsActionCopy, nullptr);
}
// Implements AtkEditableText::cut_text.
static void fl_accessible_text_field_cut_text(AtkEditableText* editable_text,
gint start_pos,
gint end_pos) {
g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(editable_text));
FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(editable_text);
perform_set_selection_action(self, start_pos, end_pos);
fl_accessible_node_perform_action(FL_ACCESSIBLE_NODE(editable_text),
kFlutterSemanticsActionCut, nullptr);
}
// Implements AtkEditableText::paste_text.
static void fl_accessible_text_field_paste_text(AtkEditableText* editable_text,
gint position) {
g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(editable_text));
FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(editable_text);
perform_set_selection_action(self, position, position);
fl_accessible_node_perform_action(FL_ACCESSIBLE_NODE(editable_text),
kFlutterSemanticsActionPaste, nullptr);
}
static void fl_accessible_text_field_class_init(
FlAccessibleTextFieldClass* klass) {
G_OBJECT_CLASS(klass)->dispose = fl_accessible_text_field_dispose;
FL_ACCESSIBLE_NODE_CLASS(klass)->set_value =
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;
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;
iface->get_n_selections = fl_accessible_text_field_get_n_selections;
iface->get_selection = fl_accessible_text_field_get_selection;
iface->add_selection = fl_accessible_text_field_add_selection;
iface->remove_selection = fl_accessible_text_field_remove_selection;
iface->set_selection = fl_accessible_text_field_set_selection;
}
static void fl_accessible_editable_text_iface_init(
AtkEditableTextIface* iface) {
iface->set_text_contents = fl_accessible_text_field_set_text_contents;
iface->insert_text = fl_accessible_text_field_insert_text;
iface->delete_text = fl_accessible_node_delete_text;
iface->copy_text = fl_accessible_text_field_copy_text;
iface->cut_text = fl_accessible_text_field_cut_text;
iface->paste_text = fl_accessible_text_field_paste_text;
}
static void fl_accessible_text_field_init(FlAccessibleTextField* self) {
self->selection_base = -1;
self->selection_extent = -1;
self->buffer = gtk_entry_buffer_new("", 0);
g_signal_connect_object(
self->buffer, "inserted-text",
G_CALLBACK(+[](FlAccessibleTextField* self, guint position, gchar* chars,
guint n_chars) {
g_signal_emit_by_name(self, "text-insert", position, n_chars, chars,
nullptr);
}),
self, G_CONNECT_SWAPPED);
g_signal_connect_object(self->buffer, "deleted-text",
G_CALLBACK(+[](FlAccessibleTextField* self,
guint position, guint n_chars) {
g_autofree gchar* chars = atk_text_get_text(
ATK_TEXT(self), position, position + n_chars);
g_signal_emit_by_name(self, "text-remove", position,
n_chars, chars, nullptr);
}),
self, G_CONNECT_SWAPPED);
}
FlAccessibleNode* fl_accessible_text_field_new(FlEngine* engine, int32_t id) {
return FL_ACCESSIBLE_NODE(g_object_new(fl_accessible_text_field_get_type(),
"engine", engine, "id", id, nullptr));
}