610 lines
21 KiB
Diff
610 lines
21 KiB
Diff
From 633a8b188558a3bc9d309ba2554ff665791eb3b7 Mon Sep 17 00:00:00 2001
|
|
From: Lukáš Tyrychtr <ltyrycht@redhat.com>
|
|
Date: Fri, 28 Mar 2025 09:54:20 +0100
|
|
Subject: libview: implement AccessibleText for PpsViewPage
|
|
|
|
Emit proper notifications from the text implementation.
|
|
|
|
Backport assisted-by Claude <noreply@anthropic.com>
|
|
|
|
diff --git a/libview/pps-view.c b/libview/pps-view.c
|
|
index ec6c1f8d7..e3152b9dc 100644
|
|
--- a/libview/pps-view.c
|
|
+++ b/libview/pps-view.c
|
|
@@ -258,7 +258,9 @@ static void pps_view_stop_signature_rect (PpsView *view);
|
|
|
|
static void pps_view_update_primary_selection (PpsView *view);
|
|
|
|
-G_DEFINE_TYPE_WITH_CODE (PpsView, pps_view, GTK_TYPE_WIDGET, G_ADD_PRIVATE (PpsView) G_IMPLEMENT_INTERFACE (GTK_TYPE_SCROLLABLE, NULL))
|
|
+static void pps_view_accessible_text_init (GtkAccessibleTextInterface *iface);
|
|
+
|
|
+G_DEFINE_TYPE_WITH_CODE (PpsView, pps_view, GTK_TYPE_WIDGET, G_ADD_PRIVATE (PpsView) G_IMPLEMENT_INTERFACE (GTK_TYPE_SCROLLABLE, NULL) G_IMPLEMENT_INTERFACE (GTK_TYPE_ACCESSIBLE_TEXT, pps_view_accessible_text_init))
|
|
|
|
#define GET_PRIVATE(o) pps_view_get_instance_private (o)
|
|
|
|
@@ -3855,6 +3857,7 @@ pps_view_set_caret_cursor_position (PpsView *view,
|
|
|
|
g_signal_emit (view, signals[SIGNAL_CURSOR_MOVED], 0,
|
|
priv->cursor_page, priv->cursor_offset);
|
|
+ gtk_accessible_text_update_caret_position (GTK_ACCESSIBLE_TEXT (view));
|
|
|
|
if (priv->caret_enabled && cursor_is_in_visible_page (view))
|
|
gtk_widget_queue_draw (GTK_WIDGET (view));
|
|
@@ -4910,6 +4913,7 @@ position_caret_cursor_for_event (PpsView *view,
|
|
priv->cursor_line_offset = area.x;
|
|
|
|
g_signal_emit (view, signals[SIGNAL_CURSOR_MOVED], 0, priv->cursor_page, priv->cursor_offset);
|
|
+ gtk_accessible_text_update_caret_position (GTK_ACCESSIBLE_TEXT (view));
|
|
|
|
if (redraw) {
|
|
gtk_widget_queue_draw (GTK_WIDGET (view));
|
|
@@ -6197,6 +6201,7 @@ pps_view_move_cursor (PpsView *view,
|
|
rect.y += scroll_y;
|
|
_pps_view_ensure_rectangle_is_visible (view, priv->cursor_page, &rect);
|
|
g_signal_emit (view, signals[SIGNAL_CURSOR_MOVED], 0, priv->cursor_page, priv->cursor_offset);
|
|
+ gtk_accessible_text_update_caret_position (GTK_ACCESSIBLE_TEXT (view));
|
|
clear_selection (view);
|
|
return TRUE;
|
|
}
|
|
@@ -6236,6 +6241,7 @@ pps_view_move_cursor (PpsView *view,
|
|
_pps_view_ensure_rectangle_is_visible (view, priv->cursor_page, &rect);
|
|
|
|
g_signal_emit (view, signals[SIGNAL_CURSOR_MOVED], 0, priv->cursor_page, priv->cursor_offset);
|
|
+ gtk_accessible_text_update_caret_position (GTK_ACCESSIBLE_TEXT (view));
|
|
|
|
gtk_widget_queue_draw (GTK_WIDGET (view));
|
|
|
|
@@ -7026,6 +7032,541 @@ zoom_gesture_scale_changed_cb (GtkGestureZoom *gesture,
|
|
pps_view_zoom (view, factor);
|
|
}
|
|
|
|
+/* ATs expect to be able to identify sentence boundaries based on content. Valid,
|
|
+ * content-based boundaries may be present at the end of a newline, for instance
|
|
+ * at the end of a heading within a document. Thus being able to distinguish hard
|
|
+ * returns from soft returns is necessary. However, the text we get from Poppler
|
|
+ * for non-tagged PDFs has "\n" inserted at the end of each line resulting in a
|
|
+ * broken accessibility implementation w.r.t. sentences.
|
|
+ */
|
|
+static gboolean
|
|
+treat_as_soft_return (PpsView *self,
|
|
+ gint page,
|
|
+ PangoLogAttr *log_attrs,
|
|
+ gint offset)
|
|
+{
|
|
+ PpsViewPrivate *priv = GET_PRIVATE (self);
|
|
+ PpsRectangle *areas = NULL;
|
|
+ guint n_areas = 0;
|
|
+ gdouble line_spacing, this_line_height, next_word_width;
|
|
+ PpsRectangle *this_line_start;
|
|
+ PpsRectangle *this_line_end;
|
|
+ PpsRectangle *next_line_start;
|
|
+ PpsRectangle *next_line_end;
|
|
+ PpsRectangle *next_word_end;
|
|
+ gint prev_offset, next_offset;
|
|
+
|
|
+ if (!log_attrs[offset].is_white)
|
|
+ return FALSE;
|
|
+
|
|
+ pps_page_cache_get_text_layout (priv->page_cache, page, &areas, &n_areas);
|
|
+ if (n_areas <= offset + 1)
|
|
+ return FALSE;
|
|
+
|
|
+ prev_offset = offset - 1;
|
|
+ next_offset = offset + 1;
|
|
+
|
|
+ /* In wrapped text, the character at the start of the next line starts a word.
|
|
+ * Examples where this condition might fail include bullets and images. But it
|
|
+ * also includes things like "(", so also check the next character.
|
|
+ */
|
|
+ if (!log_attrs[next_offset].is_word_start &&
|
|
+ (next_offset + 1 >= n_areas || !log_attrs[next_offset + 1].is_word_start))
|
|
+ return FALSE;
|
|
+
|
|
+ /* In wrapped text, the chars on either side of the newline have very similar heights.
|
|
+ * Examples where this condition might fail include a newline at the end of a heading,
|
|
+ * and a newline at the end of a paragraph that is followed by a heading.
|
|
+ */
|
|
+ this_line_end = areas + prev_offset;
|
|
+ next_line_start = areas + next_offset;
|
|
+
|
|
+ this_line_height = this_line_end->y2 - this_line_end->y1;
|
|
+ if (ABS (this_line_height - (next_line_start->y2 - next_line_start->y1)) > 0.25)
|
|
+ return FALSE;
|
|
+
|
|
+ /* If there is significant white space between this line and the next, odds are this
|
|
+ * is not a soft return in wrapped text. Lines within a typical paragraph are at most
|
|
+ * double-spaced. If the spacing is more than that, assume a hard return is present.
|
|
+ */
|
|
+ line_spacing = next_line_start->y1 - this_line_end->y2;
|
|
+ if (line_spacing - this_line_height > 1)
|
|
+ return FALSE;
|
|
+
|
|
+ /* Lines within a typical paragraph have *reasonably* similar x1 coordinates. But
|
|
+ * we cannot count on them being nearly identical. Examples where indentation can
|
|
+ * be present in wrapped text include indenting the first line of the paragraph,
|
|
+ * and hanging indents (e.g. in the works cited within an academic paper). So we'll
|
|
+ * be somewhat tolerant here.
|
|
+ */
|
|
+ while (prev_offset > 0 && !log_attrs[prev_offset].is_mandatory_break)
|
|
+ prev_offset--;
|
|
+ this_line_start = areas + prev_offset;
|
|
+ if (ABS (this_line_start->x1 - next_line_start->x1) > 20)
|
|
+ return FALSE;
|
|
+
|
|
+ /* Ditto for x2, but this line might be short due to a wide word on the next line. */
|
|
+ while (next_offset < n_areas && !log_attrs[next_offset].is_word_end)
|
|
+ next_offset++;
|
|
+ next_word_end = areas + next_offset;
|
|
+ next_word_width = next_word_end->x2 - next_line_start->x1;
|
|
+
|
|
+ while (next_offset < n_areas && !log_attrs[next_offset + 1].is_mandatory_break)
|
|
+ next_offset++;
|
|
+ next_line_end = areas + next_offset;
|
|
+ if (next_line_end->x2 - (this_line_end->x2 + next_word_width) > 20)
|
|
+ return FALSE;
|
|
+
|
|
+ return TRUE;
|
|
+}
|
|
+
|
|
+static gchar *
|
|
+get_substring (GtkAccessibleText *text,
|
|
+ unsigned int start,
|
|
+ unsigned int end)
|
|
+{
|
|
+ PpsView *self = PPS_VIEW (text);
|
|
+ PpsViewPrivate *priv = GET_PRIVATE (self);
|
|
+ gchar *substring, *normalized;
|
|
+ const gchar *page_text;
|
|
+
|
|
+ page_text = pps_page_cache_get_text (priv->page_cache, priv->cursor_page);
|
|
+ if (!page_text)
|
|
+ return NULL;
|
|
+ if (end > g_utf8_strlen (page_text, -1))
|
|
+ end = strlen (page_text);
|
|
+ start = CLAMP (start, 0, end);
|
|
+
|
|
+ substring = g_utf8_substring (page_text, start, end);
|
|
+ normalized = g_utf8_normalize (substring, -1, G_NORMALIZE_NFKC);
|
|
+ g_free (substring);
|
|
+
|
|
+ return normalized;
|
|
+}
|
|
+
|
|
+static GBytes *
|
|
+pps_view_accessible_text_get_contents (GtkAccessibleText *text,
|
|
+ unsigned int start,
|
|
+ unsigned int end)
|
|
+{
|
|
+ gchar *substring = get_substring (text, start, end);
|
|
+ if (!substring)
|
|
+ return g_bytes_new (NULL, 0);
|
|
+
|
|
+ return g_bytes_new_take (substring, strlen (substring));
|
|
+}
|
|
+
|
|
+static void
|
|
+get_range_for_granularity (GtkAccessibleText *text,
|
|
+ GtkAccessibleTextGranularity granularity,
|
|
+ unsigned int offset,
|
|
+ unsigned int *start_offset,
|
|
+ unsigned int *end_offset)
|
|
+{
|
|
+ PpsView *self = PPS_VIEW (text);
|
|
+ PpsViewPrivate *priv = GET_PRIVATE (self);
|
|
+ gint start = 0;
|
|
+ gint end = 0;
|
|
+ PangoLogAttr *log_attrs = NULL;
|
|
+ gulong n_attrs;
|
|
+
|
|
+ if (!priv->page_cache)
|
|
+ return;
|
|
+
|
|
+ pps_page_cache_get_text_log_attrs (priv->page_cache, priv->cursor_page, &log_attrs, &n_attrs);
|
|
+ if (!log_attrs)
|
|
+ return;
|
|
+
|
|
+ if (offset >= n_attrs)
|
|
+ return;
|
|
+
|
|
+ switch (granularity) {
|
|
+ case GTK_ACCESSIBLE_TEXT_GRANULARITY_CHARACTER:
|
|
+ start = offset;
|
|
+ end = offset + 1;
|
|
+ break;
|
|
+ case GTK_ACCESSIBLE_TEXT_GRANULARITY_WORD:
|
|
+ start = offset;
|
|
+ while (start > 0 && !log_attrs[start].is_word_start)
|
|
+ start--;
|
|
+ end = offset + 1;
|
|
+ while (end < n_attrs && !log_attrs[end].is_word_start)
|
|
+ end++;
|
|
+ break;
|
|
+ case GTK_ACCESSIBLE_TEXT_GRANULARITY_SENTENCE:
|
|
+ for (start = offset; start > 0; start--) {
|
|
+ if (log_attrs[start].is_mandatory_break && treat_as_soft_return (self, priv->cursor_page, log_attrs, start - 1))
|
|
+ continue;
|
|
+ if (log_attrs[start].is_sentence_start)
|
|
+ break;
|
|
+ }
|
|
+ for (end = offset + 1; end < n_attrs; end++) {
|
|
+ if (log_attrs[end].is_mandatory_break && treat_as_soft_return (self, priv->cursor_page, log_attrs, end - 1))
|
|
+ continue;
|
|
+ if (log_attrs[end].is_sentence_start)
|
|
+ break;
|
|
+ }
|
|
+ break;
|
|
+ case GTK_ACCESSIBLE_TEXT_GRANULARITY_LINE:
|
|
+ start = offset;
|
|
+ while (start > 0 && !log_attrs[start].is_mandatory_break)
|
|
+ start--;
|
|
+ end = offset + 1;
|
|
+ while (end < n_attrs && !log_attrs[end].is_mandatory_break)
|
|
+ end++;
|
|
+ break;
|
|
+ case GTK_ACCESSIBLE_TEXT_GRANULARITY_PARAGRAPH:
|
|
+ /* FIXME: There is likely more than one paragraph on the page, so try to deduce it properly */
|
|
+ start = 0;
|
|
+ end = n_attrs;
|
|
+ }
|
|
+
|
|
+ *start_offset = start;
|
|
+ *end_offset = end;
|
|
+}
|
|
+
|
|
+static GBytes *
|
|
+pps_view_accessible_text_get_contents_at (GtkAccessibleText *text,
|
|
+ unsigned int offset,
|
|
+ GtkAccessibleTextGranularity granularity,
|
|
+ unsigned int *start_offset,
|
|
+ unsigned int *end_offset)
|
|
+{
|
|
+ gchar *substring;
|
|
+
|
|
+ get_range_for_granularity (text, granularity, offset, start_offset, end_offset);
|
|
+ substring = get_substring (text, *start_offset, *end_offset);
|
|
+
|
|
+ /* If newlines appear inside the text of a sentence (i.e. between the start and
|
|
+ * end offsets returned by get_substring), it interferes with
|
|
+ * the prosody of text-to-speech based-solutions such as a screen reader because
|
|
+ * speech synthesizers tend to pause after the newline char as if it were the end
|
|
+ * of the sentence.
|
|
+ */
|
|
+ if (granularity == GTK_ACCESSIBLE_TEXT_GRANULARITY_SENTENCE)
|
|
+ g_strdelimit (substring, "\n", ' ');
|
|
+
|
|
+ return g_bytes_new_take (substring, strlen (substring));
|
|
+}
|
|
+
|
|
+static unsigned int
|
|
+pps_view_accessible_text_get_caret_position (GtkAccessibleText *text)
|
|
+{
|
|
+ PpsView *self = PPS_VIEW (text);
|
|
+ PpsViewPrivate *priv = GET_PRIVATE (self);
|
|
+
|
|
+ if (priv->caret_enabled)
|
|
+ return priv->cursor_offset;
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static gboolean
|
|
+get_selection_bounds (PpsView *self,
|
|
+ gint *start_offset,
|
|
+ gint *end_offset)
|
|
+{
|
|
+ PpsViewPrivate *priv = GET_PRIVATE (self);
|
|
+ cairo_rectangle_int_t rect;
|
|
+ cairo_region_t *region;
|
|
+ gint start, end;
|
|
+ gdouble scale = pps_document_model_get_scale (priv->model);
|
|
+
|
|
+ region = pps_pixbuf_cache_get_selection_region (priv->pixbuf_cache,
|
|
+ priv->cursor_page,
|
|
+ scale);
|
|
+
|
|
+ if (!region || cairo_region_is_empty (region))
|
|
+ return FALSE;
|
|
+
|
|
+ cairo_region_get_rectangle (region, 0, &rect);
|
|
+ start = _pps_view_get_caret_cursor_offset_at_doc_point (self,
|
|
+ priv->cursor_page,
|
|
+ rect.x / scale,
|
|
+ (rect.y + (rect.height / 2)) / scale);
|
|
+ if (start == -1)
|
|
+ return FALSE;
|
|
+
|
|
+ cairo_region_get_rectangle (region,
|
|
+ cairo_region_num_rectangles (region) - 1,
|
|
+ &rect);
|
|
+ end = _pps_view_get_caret_cursor_offset_at_doc_point (self,
|
|
+ priv->cursor_page,
|
|
+ (rect.x + rect.width) / scale,
|
|
+ (rect.y + (rect.height / 2)) / scale);
|
|
+ if (end == -1)
|
|
+ return FALSE;
|
|
+
|
|
+ *start_offset = start;
|
|
+ *end_offset = end;
|
|
+
|
|
+ return TRUE;
|
|
+}
|
|
+
|
|
+static gboolean
|
|
+pps_view_accessible_text_get_selection (GtkAccessibleText *text,
|
|
+ gsize *n_ranges,
|
|
+ GtkAccessibleTextRange **ranges)
|
|
+{
|
|
+ PpsView *self = PPS_VIEW (text);
|
|
+
|
|
+ *n_ranges = 0;
|
|
+
|
|
+ gint start = 0, end = 0;
|
|
+ if (get_selection_bounds (self, &start, &end) && start != end) {
|
|
+ *n_ranges = 1;
|
|
+ *ranges = g_new (GtkAccessibleTextRange, 1);
|
|
+ (*ranges)[0].start = start;
|
|
+ (*ranges)[0].length = end - start;
|
|
+ return TRUE;
|
|
+ }
|
|
+
|
|
+ return FALSE;
|
|
+}
|
|
+
|
|
+static void
|
|
+fill_run_attributes (PangoAttrList *attrs,
|
|
+ const gchar *text,
|
|
+ unsigned int offset,
|
|
+ unsigned int *start_offset,
|
|
+ unsigned int *end_offset,
|
|
+ GStrvBuilder *names,
|
|
+ GStrvBuilder *values)
|
|
+{
|
|
+ PangoAttrString *pango_string;
|
|
+ PangoAttrInt *pango_int;
|
|
+ PangoAttrColor *pango_color;
|
|
+ PangoAttrIterator *iter;
|
|
+ gint i, start, end;
|
|
+ gboolean has_attrs = FALSE;
|
|
+ glong text_length;
|
|
+ gchar *attr_value;
|
|
+
|
|
+ text_length = g_utf8_strlen (text, -1);
|
|
+ if (offset < 0 || offset >= text_length)
|
|
+ return;
|
|
+
|
|
+ /* Check if there are attributes for the offset,
|
|
+ * and set the attributes range if positive */
|
|
+ iter = pango_attr_list_get_iterator (attrs);
|
|
+ i = g_utf8_offset_to_pointer (text, offset) - text;
|
|
+
|
|
+ do {
|
|
+ pango_attr_iterator_range (iter, &start, &end);
|
|
+ if (i >= start && i < end) {
|
|
+ *start_offset = g_utf8_pointer_to_offset (text, text + start);
|
|
+ if (end == G_MAXINT) /* Last iterator */
|
|
+ end = text_length;
|
|
+ *end_offset = g_utf8_pointer_to_offset (text, text + end);
|
|
+ has_attrs = TRUE;
|
|
+ }
|
|
+ } while (!has_attrs && pango_attr_iterator_next (iter));
|
|
+
|
|
+ if (!has_attrs) {
|
|
+ pango_attr_iterator_destroy (iter);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ /* Fill the GtkAccessibleText attributes from the Pango attributes */
|
|
+ pango_string = (PangoAttrString *) pango_attr_iterator_get (iter, PANGO_ATTR_FAMILY);
|
|
+ if (pango_string) {
|
|
+ g_strv_builder_add (names, GTK_ACCESSIBLE_ATTRIBUTE_FAMILY);
|
|
+ g_strv_builder_add (values, pango_string->value);
|
|
+ }
|
|
+
|
|
+ pango_int = (PangoAttrInt *) pango_attr_iterator_get (iter, PANGO_ATTR_SIZE);
|
|
+ if (pango_int) {
|
|
+ attr_value = g_strdup_printf ("%i", pango_int->value / PANGO_SCALE);
|
|
+ g_strv_builder_add (names, GTK_ACCESSIBLE_ATTRIBUTE_SIZE);
|
|
+ g_strv_builder_add (values, attr_value);
|
|
+ }
|
|
+
|
|
+ pango_int = (PangoAttrInt *) pango_attr_iterator_get (iter, PANGO_ATTR_UNDERLINE);
|
|
+ if (pango_int) {
|
|
+ g_strv_builder_add (names, GTK_ACCESSIBLE_ATTRIBUTE_UNDERLINE);
|
|
+ g_strv_builder_add (values, pango_int->value ? GTK_ACCESSIBLE_ATTRIBUTE_UNDERLINE_NONE : GTK_ACCESSIBLE_ATTRIBUTE_UNDERLINE_SINGLE);
|
|
+ }
|
|
+ pango_color = (PangoAttrColor *) pango_attr_iterator_get (iter, PANGO_ATTR_FOREGROUND);
|
|
+ if (pango_color) {
|
|
+ attr_value = g_strdup_printf ("%u,%u,%u",
|
|
+ pango_color->color.red,
|
|
+ pango_color->color.green,
|
|
+ pango_color->color.blue);
|
|
+ g_strv_builder_add (names, GTK_ACCESSIBLE_ATTRIBUTE_FOREGROUND);
|
|
+ g_strv_builder_add (values, attr_value);
|
|
+ }
|
|
+
|
|
+ pango_attr_iterator_destroy (iter);
|
|
+}
|
|
+
|
|
+static gboolean
|
|
+pps_view_accessible_text_get_attributes (GtkAccessibleText *text,
|
|
+ unsigned int offset,
|
|
+ gsize *n_ranges,
|
|
+ GtkAccessibleTextRange **ranges,
|
|
+ char ***attribute_names,
|
|
+ char ***attribute_values)
|
|
+{
|
|
+ PpsView *self = PPS_VIEW (text);
|
|
+ PpsViewPrivate *priv = GET_PRIVATE (self);
|
|
+ PangoAttrList *attrs;
|
|
+ const gchar *page_text;
|
|
+ unsigned int start_offset = 0;
|
|
+ unsigned int end_offset = 0;
|
|
+ PpsPageCache *cache = priv->page_cache;
|
|
+
|
|
+ if (offset < 0)
|
|
+ goto no_attrs;
|
|
+
|
|
+ if (!cache)
|
|
+ goto no_attrs;
|
|
+
|
|
+ page_text = pps_page_cache_get_text (cache, priv->cursor_page);
|
|
+ if (!page_text)
|
|
+ goto no_attrs;
|
|
+
|
|
+ attrs = pps_page_cache_get_text_attrs (cache, priv->cursor_page);
|
|
+ if (!attrs)
|
|
+ goto no_attrs;
|
|
+
|
|
+ GStrvBuilder *names = g_strv_builder_new ();
|
|
+ GStrvBuilder *values = g_strv_builder_new ();
|
|
+ fill_run_attributes (attrs, page_text, offset, &start_offset, &end_offset, names, values);
|
|
+ *n_ranges = 1;
|
|
+ *ranges = g_new (GtkAccessibleTextRange, 1);
|
|
+ (*ranges)[0].start = start_offset;
|
|
+ (*ranges)[0].length = end_offset - start_offset;
|
|
+ *attribute_names = g_strv_builder_end (names);
|
|
+ *attribute_values = g_strv_builder_end (values);
|
|
+ g_strv_builder_unref (names);
|
|
+ g_strv_builder_unref (values);
|
|
+ if (attribute_names[0] && attribute_names[0][0])
|
|
+ return TRUE;
|
|
+ else {
|
|
+ *n_ranges = 0;
|
|
+ return FALSE;
|
|
+ }
|
|
+no_attrs:
|
|
+ *n_ranges = 0;
|
|
+ *attribute_names = g_new0 (char *, 1);
|
|
+ *attribute_values = g_new0 (char *, 1);
|
|
+ return FALSE;
|
|
+}
|
|
+
|
|
+static void
|
|
+pps_view_accessible_text_get_default_attributes (GtkAccessibleText *text,
|
|
+ char ***attribute_names,
|
|
+ char ***attribute_values)
|
|
+{
|
|
+ /* No default attributes */
|
|
+ *attribute_names = g_new0 (char *, 1);
|
|
+ *attribute_values = g_new0 (char *, 1);
|
|
+ return;
|
|
+}
|
|
+
|
|
+static gboolean
|
|
+pps_view_accessible_text_get_extents (GtkAccessibleText *text,
|
|
+ unsigned int start,
|
|
+ unsigned int end,
|
|
+ graphene_rect_t *extents)
|
|
+{
|
|
+ PpsView *self = PPS_VIEW (text);
|
|
+ PpsViewPrivate *priv = GET_PRIVATE (self);
|
|
+ GtkWidget *toplevel;
|
|
+ PpsRectangle *areas = NULL;
|
|
+ PpsRectangle *doc_rect;
|
|
+ guint n_areas = 0;
|
|
+ graphene_point_t widget_point;
|
|
+ GdkRectangle view_rect;
|
|
+ PpsPageCache *cache = priv->page_cache;
|
|
+
|
|
+ if (!cache)
|
|
+ return FALSE;
|
|
+
|
|
+ pps_page_cache_get_text_layout (cache, priv->cursor_page, &areas, &n_areas);
|
|
+ if (!areas || start < 0 || start >= n_areas || end >= n_areas || start > end)
|
|
+ return FALSE;
|
|
+
|
|
+ extents->origin.x = 0;
|
|
+ extents->origin.y = 0;
|
|
+ extents->size.width = 0;
|
|
+ extents->size.height = 0;
|
|
+ toplevel = GTK_WIDGET (gtk_widget_get_root (GTK_WIDGET (self)));
|
|
+ for (int offset = start; offset <= end; offset++) {
|
|
+ doc_rect = areas + offset;
|
|
+ _pps_view_transform_doc_rect_to_view_rect (self, priv->cursor_page, doc_rect, &view_rect);
|
|
+
|
|
+ if (!gtk_widget_compute_point (GTK_WIDGET (self), toplevel, graphene_point_zero (), &widget_point))
|
|
+ return FALSE;
|
|
+ view_rect.x += widget_point.x;
|
|
+ view_rect.y += widget_point.y;
|
|
+
|
|
+ extents->origin.x = MAX (extents->origin.x, view_rect.x);
|
|
+ extents->origin.y = MAX (extents->origin.y, view_rect.y);
|
|
+ extents->size.width = MAX (extents->size.width, view_rect.width);
|
|
+ extents->size.height = MAX (extents->size.height, view_rect.height);
|
|
+ }
|
|
+
|
|
+ return TRUE;
|
|
+}
|
|
+
|
|
+static gboolean
|
|
+pps_view_accessible_text_get_offset (GtkAccessibleText *text,
|
|
+ const graphene_point_t *point,
|
|
+ unsigned int *offset)
|
|
+{
|
|
+ PpsView *self = PPS_VIEW (text);
|
|
+ PpsViewPrivate *priv = GET_PRIVATE (self);
|
|
+ GtkWidget *toplevel;
|
|
+ PpsRectangle *areas = NULL;
|
|
+ PpsRectangle *rect = NULL;
|
|
+ guint n_areas = 0;
|
|
+ guint i;
|
|
+ graphene_point_t widget_point;
|
|
+ gdouble view_x, view_y;
|
|
+ PpsPoint doc_point;
|
|
+ PpsPageCache *cache = priv->page_cache;
|
|
+
|
|
+ if (!cache)
|
|
+ return FALSE;
|
|
+
|
|
+ pps_page_cache_get_text_layout (cache, priv->cursor_page, &areas, &n_areas);
|
|
+ if (!areas)
|
|
+ return FALSE;
|
|
+
|
|
+ view_x = point->x;
|
|
+ view_y = point->y;
|
|
+ toplevel = GTK_WIDGET (gtk_widget_get_root (GTK_WIDGET (self)));
|
|
+ if (!gtk_widget_compute_point (GTK_WIDGET (self), toplevel, graphene_point_zero (), &widget_point))
|
|
+ return FALSE;
|
|
+ view_x -= widget_point.x;
|
|
+ view_y -= widget_point.y;
|
|
+
|
|
+ doc_point = pps_view_get_doc_point_for_page (self, priv->cursor_page, view_x, view_y);
|
|
+
|
|
+ for (i = 0; i < n_areas; i++) {
|
|
+ rect = areas + i;
|
|
+ if (doc_point.x >= rect->x1 && doc_point.x <= rect->x2 &&
|
|
+ doc_point.y >= rect->y1 && doc_point.y <= rect->y2)
|
|
+ *offset = i;
|
|
+ }
|
|
+
|
|
+ return TRUE;
|
|
+}
|
|
+
|
|
+static void
|
|
+pps_view_accessible_text_init (GtkAccessibleTextInterface *iface)
|
|
+{
|
|
+ iface->get_contents = pps_view_accessible_text_get_contents;
|
|
+ iface->get_contents_at = pps_view_accessible_text_get_contents_at;
|
|
+ iface->get_caret_position = pps_view_accessible_text_get_caret_position;
|
|
+ iface->get_selection = pps_view_accessible_text_get_selection;
|
|
+ iface->get_attributes = pps_view_accessible_text_get_attributes;
|
|
+ iface->get_default_attributes = pps_view_accessible_text_get_default_attributes;
|
|
+ iface->get_extents = pps_view_accessible_text_get_extents;
|
|
+ iface->get_offset = pps_view_accessible_text_get_offset;
|
|
+}
|
|
+
|
|
static void
|
|
pps_view_class_init (PpsViewClass *class)
|
|
{
|
|
@@ -8427,6 +8975,7 @@ merge_selection_region (PpsView *view,
|
|
priv->selection_info.selections = new_list;
|
|
pps_pixbuf_cache_set_selection_list (priv->pixbuf_cache, new_list);
|
|
g_signal_emit (view, signals[SIGNAL_SELECTION_CHANGED], 0, NULL);
|
|
+ gtk_accessible_text_update_selection_bound (GTK_ACCESSIBLE_TEXT (view));
|
|
|
|
new_list_ptr = new_list;
|
|
old_list_ptr = old_list;
|
|
--
|
|
2.51.1
|
|
|