1
0
mirror of https://gitlab.gnome.org/GNOME/evince synced 2024-07-05 00:59:07 +00:00
evince/libview/ev-page-accessible.c
Pablo Correa Gómez d68515c164 ev-page-accessible: silence maybe-uninitialized warnings
Silence the following warnings, since the compiler does not have the
warranty that start and end might have been set inside get_selection_bounds

../libview/ev-page-accessible.c: In function 'ev_page_accessible_get_selection':
../libview/ev-page-accessible.c:731:74: warning: 'start' may be used uninitialized [-Wmaybe-uninitialized]
  731 |                 if (get_selection_bounds (view, selection, &start, &end) && start != end) {
      |                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~
../libview/ev-page-accessible.c:726:22: note: 'start' was declared here
  726 |                 gint start, end;
      |                      ^~~~~
../libview/ev-page-accessible.c:731:74: warning: 'end' may be used uninitialized [-Wmaybe-uninitialized]
  731 |                 if (get_selection_bounds (view, selection, &start, &end) && start != end) {
      |                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~
../libview/ev-page-accessible.c:726:29: note: 'end' was declared here
  726 |                 gint start, end;
      |                             ^~~
}
}
2023-05-20 18:23:20 +00:00

1413 lines
42 KiB
C

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8; c-indent-level: 8 -*- */
/* this file is part of evince, a gnome document viewer
*
* Copyright (C) 2014 Igalia S.L.
*
* Evince is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* Evince 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
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Author: Alejandro Piñeiro Iglesias <apinheiro@igalia.com>
*/
#include <config.h>
#include <glib/gi18n-lib.h>
#include "ev-page-accessible.h"
#include "ev-form-field-accessible.h"
#include "ev-image-accessible.h"
#include "ev-link-accessible.h"
#include "ev-view-private.h"
struct _EvPageAccessiblePrivate {
EvViewAccessible *view_accessible;
gint page;
GHashTable *links;
GPtrArray *children;
gboolean children_initialized;
};
enum {
PROP_0,
PROP_VIEW_ACCESSIBLE,
PROP_PAGE,
};
static void ev_page_accessible_component_iface_init (AtkComponentIface *iface);
static void ev_page_accessible_hypertext_iface_init (AtkHypertextIface *iface);
static void ev_page_accessible_text_iface_init (AtkTextIface *iface);
G_DEFINE_TYPE_WITH_CODE (EvPageAccessible, ev_page_accessible, ATK_TYPE_OBJECT,
G_ADD_PRIVATE (EvPageAccessible)
G_IMPLEMENT_INTERFACE (ATK_TYPE_COMPONENT, ev_page_accessible_component_iface_init)
G_IMPLEMENT_INTERFACE (ATK_TYPE_HYPERTEXT, ev_page_accessible_hypertext_iface_init)
G_IMPLEMENT_INTERFACE (ATK_TYPE_TEXT, ev_page_accessible_text_iface_init))
gint
ev_page_accessible_get_page (EvPageAccessible *page_accessible)
{
g_return_val_if_fail (EV_IS_PAGE_ACCESSIBLE (page_accessible), -1);
return page_accessible->priv->page;
}
EvViewAccessible *
ev_page_accessible_get_view_accessible (EvPageAccessible *page_accessible)
{
g_return_val_if_fail (EV_IS_PAGE_ACCESSIBLE (page_accessible), NULL);
return page_accessible->priv->view_accessible;
}
static AtkObject *
ev_page_accessible_get_parent (AtkObject *obj)
{
EvPageAccessible *self;
g_return_val_if_fail (EV_IS_PAGE_ACCESSIBLE (obj), NULL);
self = EV_PAGE_ACCESSIBLE (obj);
return ATK_OBJECT (self->priv->view_accessible);
}
static gint
compare_mappings (EvMapping *a, EvMapping *b)
{
gdouble dx, dy;
/* Very rough heuristic for simple, non-tagged PDFs. */
dy = a->area.y1 - b->area.y1;
dx = a->area.x1 - b->area.x1;
return ABS (dy) > 10 ? dy : dx;
}
static void
ev_page_accessible_initialize_children (EvPageAccessible *self)
{
EvView *view;
EvMappingList *images;
EvMappingList *links;
EvMappingList *fields;
GList *children = NULL;
GList *list;
if (self->priv->children_initialized)
return;
view = ev_page_accessible_get_view (self);
if (!ev_page_cache_is_page_cached (view->page_cache, self->priv->page))
return;
self->priv->children_initialized = TRUE;
links = ev_page_cache_get_link_mapping (view->page_cache, self->priv->page);
images = ev_page_cache_get_image_mapping (view->page_cache, self->priv->page);
fields = ev_page_cache_get_form_field_mapping (view->page_cache, self->priv->page);
if (!links && !images && !fields)
return;
children = g_list_copy (ev_mapping_list_get_list (links));
children = g_list_concat (children, g_list_copy (ev_mapping_list_get_list (images)));
children = g_list_concat (children, g_list_copy (ev_mapping_list_get_list (fields)));
children = g_list_sort (children, (GCompareFunc) compare_mappings);
self->priv->children = g_ptr_array_new_full (g_list_length (children), (GDestroyNotify) g_object_unref);
for (list = children; list && list->data; list = list->next) {
EvMapping *mapping = list->data;
AtkObject *child = NULL;
if (links && ev_mapping_list_find (links, mapping->data)) {
EvLinkAccessible *link = ev_link_accessible_new (self, EV_LINK (mapping->data), &mapping->area);
AtkHyperlink *atk_link = atk_hyperlink_impl_get_hyperlink (ATK_HYPERLINK_IMPL (link));
child = atk_hyperlink_get_object (atk_link, 0);
} else if (images && ev_mapping_list_find (images, mapping->data))
child = ATK_OBJECT (ev_image_accessible_new (self, EV_IMAGE (mapping->data), &mapping->area));
else if (fields && ev_mapping_list_find (fields, mapping->data))
child = ATK_OBJECT (ev_form_field_accessible_new (self, EV_FORM_FIELD (mapping->data), &mapping->area));
if (child)
g_ptr_array_add (self->priv->children, child);
}
g_list_free (children);
}
static void
clear_children (EvPageAccessible *self)
{
gint i;
AtkObject *child;
if (!self->priv->children)
return;
for (i = 0; i < self->priv->children->len; i++) {
child = g_ptr_array_index (self->priv->children, i);
atk_object_notify_state_change (child, ATK_STATE_DEFUNCT, TRUE);
}
g_clear_pointer (&self->priv->children, g_ptr_array_unref);
}
static void
ev_page_accessible_finalize (GObject *object)
{
EvPageAccessiblePrivate *priv = EV_PAGE_ACCESSIBLE (object)->priv;
g_clear_pointer (&priv->links, g_hash_table_destroy);
clear_children (EV_PAGE_ACCESSIBLE (object));
G_OBJECT_CLASS (ev_page_accessible_parent_class)->finalize (object);
}
static void
ev_page_accessible_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
EvPageAccessible *accessible = EV_PAGE_ACCESSIBLE (object);
switch (prop_id) {
case PROP_VIEW_ACCESSIBLE:
accessible->priv->view_accessible = EV_VIEW_ACCESSIBLE (g_value_get_object (value));
break;
case PROP_PAGE:
accessible->priv->page = g_value_get_int (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
ev_page_accessible_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
EvPageAccessible *accessible = EV_PAGE_ACCESSIBLE (object);
switch (prop_id) {
case PROP_VIEW_ACCESSIBLE:
g_value_set_object (value, ev_page_accessible_get_view_accessible (accessible));
break;
case PROP_PAGE:
g_value_set_int (value, ev_page_accessible_get_page (accessible));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
/*
* We redefine atk_class->ref_relation_set instead of just calling
* atk_object_add_relationship on ev_page_accessible_new because at
* that moment not all the pages could be created, being easier add
* the relation on demand.
*/
static AtkRelationSet *
ev_page_accessible_ref_relation_set (AtkObject *accessible)
{
gint n_pages;
EvPageAccessible *self;
AtkRelationSet *relation_set;
AtkObject *accessible_array[1];
AtkRelation *relation;
g_return_val_if_fail (EV_IS_PAGE_ACCESSIBLE (accessible), NULL);
self = EV_PAGE_ACCESSIBLE (accessible);
relation_set = ATK_OBJECT_CLASS (ev_page_accessible_parent_class)->ref_relation_set (accessible);
if (relation_set == NULL)
return NULL;
n_pages = ev_view_accessible_get_n_pages (self->priv->view_accessible);
if (n_pages == 0)
return relation_set;
if ((self->priv->page + 1) < n_pages && !atk_relation_set_contains (relation_set, ATK_RELATION_FLOWS_TO)) {
AtkObject *next_page;
next_page = atk_object_ref_accessible_child (ATK_OBJECT (self->priv->view_accessible),
self->priv->page + 1);
accessible_array [0] = next_page;
relation = atk_relation_new (accessible_array, 1, ATK_RELATION_FLOWS_TO);
atk_relation_set_add (relation_set, relation);
g_object_unref (relation);
g_object_unref (next_page);
}
if (self->priv->page > 0 && !atk_relation_set_contains (relation_set, ATK_RELATION_FLOWS_FROM)) {
AtkObject *prev_page;
prev_page = atk_object_ref_accessible_child (ATK_OBJECT (self->priv->view_accessible),
self->priv->page - 1);
accessible_array [0] = prev_page;
relation = atk_relation_new (accessible_array, 1, ATK_RELATION_FLOWS_FROM);
atk_relation_set_add (relation_set, relation);
g_object_unref (relation);
g_object_unref (prev_page);
}
return relation_set;
}
/* page accessible's state set is a copy ev-view accessible's state
* set but removing ATK_STATE_SHOWING if the page is not on screen and
* ATK_STATE_FOCUSED if it is not the relevant page. */
static AtkStateSet *
ev_page_accessible_ref_state_set (AtkObject *accessible)
{
AtkStateSet *state_set;
AtkStateSet *copy_set;
AtkStateSet *view_accessible_state_set;
EvPageAccessible *self;
EvView *view;
gint relevant_page;
g_return_val_if_fail (EV_IS_PAGE_ACCESSIBLE (accessible), NULL);
self = EV_PAGE_ACCESSIBLE (accessible);
view = ev_page_accessible_get_view (self);
state_set = ATK_OBJECT_CLASS (ev_page_accessible_parent_class)->ref_state_set (accessible);
atk_state_set_clear_states (state_set);
view_accessible_state_set = atk_object_ref_state_set (ATK_OBJECT (self->priv->view_accessible));
copy_set = atk_state_set_or_sets (state_set, view_accessible_state_set);
if (self->priv->page >= view->start_page && self->priv->page <= view->end_page)
atk_state_set_add_state (copy_set, ATK_STATE_SHOWING);
else
atk_state_set_remove_state (copy_set, ATK_STATE_SHOWING);
relevant_page = ev_view_accessible_get_relevant_page (self->priv->view_accessible);
if (atk_state_set_contains_state (view_accessible_state_set, ATK_STATE_FOCUSED) &&
self->priv->page == relevant_page)
atk_state_set_add_state (copy_set, ATK_STATE_FOCUSED);
else
atk_state_set_remove_state (copy_set, ATK_STATE_FOCUSED);
relevant_page = ev_view_accessible_get_relevant_page (self->priv->view_accessible);
if (atk_state_set_contains_state (view_accessible_state_set, ATK_STATE_FOCUSED) &&
self->priv->page == relevant_page)
atk_state_set_add_state (copy_set, ATK_STATE_FOCUSED);
else
atk_state_set_remove_state (copy_set, ATK_STATE_FOCUSED);
g_object_unref (state_set);
g_object_unref (view_accessible_state_set);
return copy_set;
}
static gint
ev_page_accessible_get_n_children (AtkObject *accessible)
{
EvPageAccessible *self;
self = EV_PAGE_ACCESSIBLE (accessible);
return self->priv->children == NULL ? 0 : self->priv->children->len;
}
static AtkObject *
ev_page_accessible_ref_child (AtkObject *accessible,
gint i)
{
EvPageAccessible *self;
self = EV_PAGE_ACCESSIBLE (accessible);
g_return_val_if_fail (i >= 0 || i < self->priv->children->len, NULL);
return g_object_ref (g_ptr_array_index (self->priv->children, i));
}
static void
ev_page_accessible_class_init (EvPageAccessibleClass *klass)
{
GObjectClass *g_object_class = G_OBJECT_CLASS (klass);
AtkObjectClass *atk_class = ATK_OBJECT_CLASS (klass);
atk_class->get_parent = ev_page_accessible_get_parent;
atk_class->ref_relation_set = ev_page_accessible_ref_relation_set;
atk_class->ref_state_set = ev_page_accessible_ref_state_set;
atk_class->get_n_children = ev_page_accessible_get_n_children;
atk_class->ref_child = ev_page_accessible_ref_child;
g_object_class->get_property = ev_page_accessible_get_property;
g_object_class->set_property = ev_page_accessible_set_property;
g_object_class->finalize = ev_page_accessible_finalize;
g_object_class_install_property (g_object_class,
PROP_VIEW_ACCESSIBLE,
g_param_spec_object ("view-accessible",
"View Accessible",
"The view accessible associated to this page",
EV_TYPE_VIEW_ACCESSIBLE,
G_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS));
g_object_class_install_property (g_object_class,
PROP_PAGE,
g_param_spec_int ("page",
"Page",
"Page index this page represents",
-1, G_MAXINT, -1,
G_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS));
}
EvView *
ev_page_accessible_get_view (EvPageAccessible *page_accessible)
{
g_return_val_if_fail (EV_IS_PAGE_ACCESSIBLE (page_accessible), NULL);
return EV_VIEW (gtk_accessible_get_widget (GTK_ACCESSIBLE (page_accessible->priv->view_accessible)));
}
/* 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 (EvView *view,
gint page,
PangoLogAttr *log_attrs,
gint offset)
{
EvRectangle *areas = NULL;
guint n_areas = 0;
gdouble line_spacing, this_line_height, next_word_width;
EvRectangle *this_line_start;
EvRectangle *this_line_end;
EvRectangle *next_line_start;
EvRectangle *next_line_end;
EvRectangle *next_word_end;
gint prev_offset, next_offset;
if (!log_attrs[offset].is_white)
return FALSE;
ev_page_cache_get_text_layout (view->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.
*/
for ( ; 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. */
for ( ; 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;
for ( ; 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 *
ev_page_accessible_get_substring (AtkText *text,
gint start_offset,
gint end_offset)
{
EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text);
EvView *view = ev_page_accessible_get_view (self);
gchar *substring, *normalized;
const gchar* page_text;
if (!view->page_cache)
return NULL;
page_text = ev_page_cache_get_text (view->page_cache, self->priv->page);
if (end_offset < 0 || end_offset > g_utf8_strlen (page_text, -1))
end_offset = strlen (page_text);
start_offset = CLAMP (start_offset, 0, end_offset);
substring = g_utf8_substring (page_text, start_offset, end_offset);
normalized = g_utf8_normalize (substring, -1, G_NORMALIZE_NFKC);
g_free (substring);
return normalized;
}
static gchar *
ev_page_accessible_get_text (AtkText *text,
gint start_pos,
gint end_pos)
{
return ev_page_accessible_get_substring (text, start_pos, end_pos);
}
static gunichar
ev_page_accessible_get_character_at_offset (AtkText *text,
gint offset)
{
gchar *string;
gunichar unichar;
string = ev_page_accessible_get_substring (text, offset, offset + 1);
unichar = g_utf8_get_char (string);
g_free(string);
return unichar;
}
static void
ev_page_accessible_get_range_for_boundary (AtkText *text,
AtkTextBoundary boundary_type,
gint offset,
gint *start_offset,
gint *end_offset)
{
EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text);
EvView *view = ev_page_accessible_get_view (self);
gint start = 0;
gint end = 0;
PangoLogAttr *log_attrs = NULL;
gulong n_attrs;
if (!view->page_cache)
return;
ev_page_cache_get_text_log_attrs (view->page_cache, self->priv->page, &log_attrs, &n_attrs);
if (!log_attrs)
return;
if (offset < 0 || offset >= n_attrs)
return;
switch (boundary_type) {
case ATK_TEXT_BOUNDARY_CHAR:
start = offset;
end = offset + 1;
break;
case ATK_TEXT_BOUNDARY_WORD_START:
for (start = offset; start > 0 && !log_attrs[start].is_word_start; start--);
for (end = offset + 1; end < n_attrs && !log_attrs[end].is_word_start; end++);
break;
case ATK_TEXT_BOUNDARY_SENTENCE_START:
for (start = offset; start > 0; start--) {
if (log_attrs[start].is_mandatory_break && treat_as_soft_return (view, self->priv->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 (view, self->priv->page, log_attrs, end - 1))
continue;
if (log_attrs[end].is_sentence_start)
break;
}
break;
case ATK_TEXT_BOUNDARY_LINE_START:
for (start = offset; start > 0 && !log_attrs[start].is_mandatory_break; start--);
for (end = offset + 1; end < n_attrs && !log_attrs[end].is_mandatory_break; end++);
break;
default:
/* The "END" boundary types are deprecated */
break;
}
*start_offset = start;
*end_offset = end;
}
static gchar *
ev_page_accessible_get_text_at_offset (AtkText *text,
gint offset,
AtkTextBoundary boundary_type,
gint *start_offset,
gint *end_offset)
{
gchar *retval;
ev_page_accessible_get_range_for_boundary (text, boundary_type, offset, start_offset, end_offset);
retval = ev_page_accessible_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 ev_page_accessible_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 (boundary_type == ATK_TEXT_BOUNDARY_SENTENCE_START)
g_strdelimit (retval, "\n", ' ');
return retval;
}
static gint
ev_page_accessible_get_caret_offset (AtkText *text)
{
EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text);
EvView *view = ev_page_accessible_get_view (self);
if (self->priv->page == view->cursor_page && view->caret_enabled)
return view->cursor_offset;
return -1;
}
static gboolean
ev_page_accessible_set_caret_offset (AtkText *text, gint offset)
{
EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text);
EvView *view = ev_page_accessible_get_view (self);
ev_view_set_caret_cursor_position (view,
self->priv->page,
offset);
return TRUE;
}
static gint
ev_page_accessible_get_character_count (AtkText *text)
{
EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text);
EvView *view = ev_page_accessible_get_view (self);
gint retval;
retval = g_utf8_strlen (ev_page_cache_get_text (view->page_cache, self->priv->page), -1);
return retval;
}
static gboolean
get_selection_bounds (EvView *view,
EvViewSelection *selection,
gint *start_offset,
gint *end_offset)
{
cairo_rectangle_int_t rect;
gint start, end;
if (!selection->covered_region || cairo_region_is_empty (selection->covered_region))
return FALSE;
cairo_region_get_rectangle (selection->covered_region, 0, &rect);
start = _ev_view_get_caret_cursor_offset_at_doc_point (view,
selection->page,
rect.x / view->scale,
(rect.y + (rect.height / 2)) / view->scale);
if (start == -1)
return FALSE;
cairo_region_get_rectangle (selection->covered_region,
cairo_region_num_rectangles (selection->covered_region) - 1,
&rect);
end = _ev_view_get_caret_cursor_offset_at_doc_point (view,
selection->page,
(rect.x + rect.width) / view->scale,
(rect.y + (rect.height / 2)) / view->scale);
if (end == -1)
return FALSE;
*start_offset = start;
*end_offset = end;
return TRUE;
}
static gint
ev_page_accessible_get_n_selections (AtkText *text)
{
EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text);
EvView *view = ev_page_accessible_get_view (self);
gint n_selections = 0;
GList *l;
if (!EV_IS_SELECTION (view->document) || !view->selection_info.selections)
return 0;
for (l = view->selection_info.selections; l != NULL; l = l->next) {
EvViewSelection *selection = (EvViewSelection *)l->data;
if (selection->page != self->priv->page)
continue;
n_selections = 1;
break;
}
return n_selections;
}
static gchar *
ev_page_accessible_get_selection (AtkText *text,
gint selection_num,
gint *start_pos,
gint *end_pos)
{
EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text);
EvView *view = ev_page_accessible_get_view (self);
gchar *selected_text = NULL;
gchar *normalized_text = NULL;
GList *l;
*start_pos = -1;
*end_pos = -1;
if (selection_num != 0)
return NULL;
if (!EV_IS_SELECTION (view->document) || !view->selection_info.selections)
return NULL;
for (l = view->selection_info.selections; l != NULL; l = l->next) {
EvViewSelection *selection = (EvViewSelection *)l->data;
gint start = 0, end = 0;
if (selection->page != self->priv->page)
continue;
if (get_selection_bounds (view, selection, &start, &end) && start != end) {
EvPage *page;
page = ev_document_get_page (view->document, selection->page);
ev_document_doc_mutex_lock ();
selected_text = ev_selection_get_selected_text (EV_SELECTION (view->document),
page,
selection->style,
&(selection->rect));
ev_document_doc_mutex_unlock ();
g_object_unref (page);
*start_pos = start;
*end_pos = end;
}
break;
}
if (selected_text) {
normalized_text = g_utf8_normalize (selected_text, -1, G_NORMALIZE_NFKC);
g_free (selected_text);
}
return normalized_text;
}
static AtkAttributeSet *
add_attribute (AtkAttributeSet *attr_set,
AtkTextAttribute attr_type,
gchar *attr_value)
{
AtkAttribute *attr = g_new (AtkAttribute, 1);
attr->name = g_strdup (atk_text_attribute_get_name (attr_type));
attr->value = attr_value;
return g_slist_prepend (attr_set, attr);
}
static AtkAttributeSet *
get_run_attributes (PangoAttrList *attrs,
const gchar *text,
gint offset,
gint *start_offset,
gint *end_offset)
{
AtkAttributeSet *atk_attr_set = NULL;
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 NULL;
/* 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 NULL;
}
/* Create the AtkAttributeSet from the Pango attributes */
pango_string = (PangoAttrString *) pango_attr_iterator_get (iter, PANGO_ATTR_FAMILY);
if (pango_string) {
attr_value = g_strdup (pango_string->value);
atk_attr_set = add_attribute (atk_attr_set, ATK_TEXT_ATTR_FAMILY_NAME, attr_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);
atk_attr_set = add_attribute (atk_attr_set, ATK_TEXT_ATTR_SIZE, attr_value);
}
pango_int = (PangoAttrInt *) pango_attr_iterator_get (iter, PANGO_ATTR_UNDERLINE);
if (pango_int) {
atk_attr_set = add_attribute (atk_attr_set,
ATK_TEXT_ATTR_UNDERLINE,
g_strdup (atk_text_attribute_get_value (ATK_TEXT_ATTR_UNDERLINE,
pango_int->value)));
}
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);
atk_attr_set = add_attribute (atk_attr_set, ATK_TEXT_ATTR_FG_COLOR, attr_value);
}
pango_attr_iterator_destroy (iter);
return atk_attr_set;
}
static AtkAttributeSet*
ev_page_accessible_get_run_attributes (AtkText *text,
gint offset,
gint *start_offset,
gint *end_offset)
{
EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text);
EvView *view = ev_page_accessible_get_view (self);
PangoAttrList *attrs;
const gchar *page_text;
if (offset < 0)
return NULL;
if (!view->page_cache)
return NULL;
page_text = ev_page_cache_get_text (view->page_cache, self->priv->page);
if (!page_text)
return NULL;
attrs = ev_page_cache_get_text_attrs (view->page_cache, self->priv->page);
if (!attrs)
return NULL;
return get_run_attributes (attrs, page_text, offset, start_offset, end_offset);
}
static AtkAttributeSet*
ev_page_accessible_get_default_attributes (AtkText *text)
{
/* No default attributes */
return NULL;
}
static void
ev_page_accessible_get_character_extents (AtkText *text,
gint offset,
gint *x,
gint *y,
gint *width,
gint *height,
AtkCoordType coords)
{
EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text);
EvView *view = ev_page_accessible_get_view (self);
GtkWidget *toplevel;
EvRectangle *areas = NULL;
EvRectangle *doc_rect;
guint n_areas = 0;
gint x_widget, y_widget;
GdkRectangle view_rect;
if (!view->page_cache)
return;
ev_page_cache_get_text_layout (view->page_cache, self->priv->page, &areas, &n_areas);
if (!areas || offset >= n_areas)
return;
doc_rect = areas + offset;
_ev_view_transform_doc_rect_to_view_rect (view, self->priv->page, doc_rect, &view_rect);
view_rect.x -= view->scroll_x;
view_rect.y -= view->scroll_y;
toplevel = gtk_widget_get_toplevel (GTK_WIDGET (view));
gtk_widget_translate_coordinates (GTK_WIDGET (view), toplevel, 0, 0, &x_widget, &y_widget);
view_rect.x += x_widget;
view_rect.y += y_widget;
if (coords == ATK_XY_SCREEN) {
gint x_window, y_window;
gdk_window_get_origin (gtk_widget_get_window (toplevel), &x_window, &y_window);
view_rect.x += x_window;
view_rect.y += y_window;
}
*x = view_rect.x;
*y = view_rect.y;
*width = view_rect.width;
*height = view_rect.height;
}
static gint
ev_page_accessible_get_offset_at_point (AtkText *text,
gint x,
gint y,
AtkCoordType coords)
{
EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text);
EvView *view = ev_page_accessible_get_view (self);
GtkWidget *toplevel;
EvRectangle *areas = NULL;
EvRectangle *rect = NULL;
guint n_areas = 0;
guint i;
gint x_widget, y_widget;
gint offset=-1;
GdkPoint view_point;
gdouble doc_x, doc_y;
GtkBorder border;
GdkRectangle page_area;
if (!view->page_cache)
return -1;
ev_page_cache_get_text_layout (view->page_cache, self->priv->page, &areas, &n_areas);
if (!areas)
return -1;
view_point.x = x;
view_point.y = y;
toplevel = gtk_widget_get_toplevel (GTK_WIDGET (view));
gtk_widget_translate_coordinates (GTK_WIDGET (view), toplevel, 0, 0, &x_widget, &y_widget);
view_point.x -= x_widget;
view_point.y -= y_widget;
if (coords == ATK_XY_SCREEN) {
gint x_window, y_window;
gdk_window_get_origin (gtk_widget_get_window (toplevel), &x_window, &y_window);
view_point.x -= x_window;
view_point.y -= y_window;
}
ev_view_get_page_extents (view, self->priv->page, &page_area, &border);
_ev_view_transform_view_point_to_doc_point (view, &view_point, &page_area, &border, &doc_x, &doc_y);
for (i = 0; i < n_areas; i++) {
rect = areas + i;
if (doc_x >= rect->x1 && doc_x <= rect->x2 &&
doc_y >= rect->y1 && doc_y <= rect->y2)
offset = i;
}
return offset;
}
/* ATK allows for multiple, non-contiguous selections within a single AtkText
* object. Unless and until Evince supports this, selection numbers are ignored.
*/
static gboolean
ev_page_accessible_remove_selection (AtkText *text,
gint selection_num)
{
EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text);
EvView *view = ev_page_accessible_get_view (self);
if (ev_view_get_has_selection (view)) {
_ev_view_clear_selection (view);
return TRUE;
}
return FALSE;
}
static gboolean
ev_page_accessible_set_selection (AtkText *text,
gint selection_num,
gint start_pos,
gint end_pos)
{
EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text);
EvView *view = ev_page_accessible_get_view (self);
EvRectangle *areas = NULL;
guint n_areas = 0;
GdkRectangle start_rect, end_rect;
GdkPoint start_point, end_point;
ev_page_cache_get_text_layout (view->page_cache, self->priv->page, &areas, &n_areas);
if (start_pos < 0 || end_pos >= n_areas)
return FALSE;
_ev_view_transform_doc_rect_to_view_rect (view, self->priv->page, areas + start_pos, &start_rect);
_ev_view_transform_doc_rect_to_view_rect (view, self->priv->page, areas + end_pos - 1, &end_rect);
start_point.x = start_rect.x;
start_point.y = start_rect.y;
end_point.x = end_rect.x + end_rect.width;
end_point.y = end_rect.y + end_rect.height;
_ev_view_set_selection (view, &start_point, &end_point);
return TRUE;
}
static gboolean
ev_page_accessible_add_selection (AtkText *text,
gint start_pos,
gint end_pos)
{
return ev_page_accessible_set_selection (text, 0, start_pos, end_pos);
}
#if ATK_CHECK_VERSION (2, 32, 0)
static gboolean
ev_page_accessible_scroll_substring_to (AtkText *text,
gint start_pos,
gint end_pos,
AtkScrollType type)
{
EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text);
EvView *view = ev_page_accessible_get_view (self);
GdkRectangle start_rect, end_rect;
GdkPoint start_point, end_point;
gdouble hpage_size, vpage_size;
EvRectangle *areas = NULL;
guint n_areas = 0;
if (end_pos < start_pos)
return FALSE;
ev_page_cache_get_text_layout (view->page_cache, self->priv->page, &areas, &n_areas);
if (start_pos < 0 || end_pos >= n_areas)
return FALSE;
_ev_view_transform_doc_rect_to_view_rect (view, self->priv->page, areas + start_pos, &start_rect);
_ev_view_transform_doc_rect_to_view_rect (view, self->priv->page, areas + end_pos - 1, &end_rect);
start_point.x = start_rect.x;
start_point.y = start_rect.y;
end_point.x = end_rect.x + end_rect.width;
end_point.y = end_rect.y + end_rect.height;
hpage_size = gtk_adjustment_get_page_size (view->hadjustment);
vpage_size = gtk_adjustment_get_page_size (view->vadjustment);
switch (type) {
case ATK_SCROLL_TOP_LEFT:
gtk_adjustment_clamp_page (view->hadjustment, start_point.x, start_point.x + hpage_size);
gtk_adjustment_clamp_page (view->vadjustment, start_point.y, start_point.y + vpage_size);
break;
case ATK_SCROLL_BOTTOM_RIGHT:
gtk_adjustment_clamp_page (view->hadjustment, end_point.x - hpage_size, end_point.x);
gtk_adjustment_clamp_page (view->vadjustment, end_point.y - vpage_size, end_point.y);
break;
case ATK_SCROLL_TOP_EDGE:
gtk_adjustment_clamp_page (view->vadjustment, start_point.y, start_point.y + vpage_size);
break;
case ATK_SCROLL_BOTTOM_EDGE:
gtk_adjustment_clamp_page (view->vadjustment, end_point.y - vpage_size, end_point.y);
break;
case ATK_SCROLL_LEFT_EDGE:
gtk_adjustment_clamp_page (view->hadjustment, start_point.x, start_point.x + hpage_size);
break;
case ATK_SCROLL_RIGHT_EDGE:
gtk_adjustment_clamp_page (view->hadjustment, end_point.x - hpage_size, end_point.x);
break;
case ATK_SCROLL_ANYWHERE:
_ev_view_ensure_rectangle_is_visible (view, &end_rect);
_ev_view_ensure_rectangle_is_visible (view, &start_rect);
break;
default:
return FALSE;
}
return TRUE;
}
static gboolean
ev_page_accessible_scroll_substring_to_point (AtkText *text,
gint start_pos,
gint end_pos,
AtkCoordType coords,
gint x,
gint y)
{
EvPageAccessible *self = EV_PAGE_ACCESSIBLE (text);
EvView *view = ev_page_accessible_get_view (self);
GdkRectangle start_rect;
GdkPoint view_point, start_point;
gdouble hpage_size, vpage_size;
EvRectangle *areas = NULL;
guint n_areas = 0;
GtkWidget *toplevel;
gint x_widget, y_widget;
GtkBorder border;
GdkRectangle page_area;
gdouble doc_x, doc_y;
if (end_pos < start_pos)
return FALSE;
ev_page_cache_get_text_layout (view->page_cache, self->priv->page, &areas, &n_areas);
if (start_pos < 0 || end_pos >= n_areas)
return FALSE;
/* Assume that the API wants to place the top left of the substring at (x, y). */
_ev_view_transform_doc_rect_to_view_rect (view, self->priv->page, areas + start_pos, &start_rect);
start_point.x = start_rect.x;
start_point.y = start_rect.y;
hpage_size = gtk_adjustment_get_page_size (view->hadjustment);
vpage_size = gtk_adjustment_get_page_size (view->vadjustment);
view_point.x = x;
view_point.y = y;
toplevel = gtk_widget_get_toplevel (GTK_WIDGET (view));
gtk_widget_translate_coordinates (GTK_WIDGET (view), toplevel, 0, 0, &x_widget, &y_widget);
view_point.x -= x_widget;
view_point.y -= y_widget;
if (coords == ATK_XY_SCREEN) {
gint x_window, y_window;
gdk_window_get_origin (gtk_widget_get_window (toplevel), &x_window, &y_window);
view_point.x -= x_window;
view_point.y -= y_window;
}
ev_view_get_page_extents (view, self->priv->page, &page_area, &border);
_ev_view_transform_view_point_to_doc_point (view, &view_point, &page_area, &border, &doc_x, &doc_y);
/* Calculate scrolling difference */
start_point.x -= doc_x;
start_point.y -= doc_y;
gtk_adjustment_clamp_page (view->hadjustment, start_point.x, start_point.x + hpage_size);
gtk_adjustment_clamp_page (view->vadjustment, start_point.y, start_point.y + vpage_size);
return TRUE;
}
#endif
static void
ev_page_accessible_init (EvPageAccessible *page)
{
atk_object_set_role (ATK_OBJECT (page), ATK_ROLE_PAGE);
page->priv = ev_page_accessible_get_instance_private (page);
}
static void
ev_page_accessible_text_iface_init (AtkTextIface *iface)
{
iface->get_text = ev_page_accessible_get_text;
iface->get_text_at_offset = ev_page_accessible_get_text_at_offset;
iface->get_character_at_offset = ev_page_accessible_get_character_at_offset;
iface->get_caret_offset = ev_page_accessible_get_caret_offset;
iface->set_caret_offset = ev_page_accessible_set_caret_offset;
iface->get_character_count = ev_page_accessible_get_character_count;
iface->get_n_selections = ev_page_accessible_get_n_selections;
iface->get_selection = ev_page_accessible_get_selection;
iface->remove_selection = ev_page_accessible_remove_selection;
iface->add_selection = ev_page_accessible_add_selection;
iface->get_run_attributes = ev_page_accessible_get_run_attributes;
iface->get_default_attributes = ev_page_accessible_get_default_attributes;
iface->get_character_extents = ev_page_accessible_get_character_extents;
iface->get_offset_at_point = ev_page_accessible_get_offset_at_point;
#if ATK_CHECK_VERSION (2, 32, 0)
iface->scroll_substring_to = ev_page_accessible_scroll_substring_to;
iface->scroll_substring_to_point = ev_page_accessible_scroll_substring_to_point;
#endif
}
static GHashTable *
ev_page_accessible_get_links (EvPageAccessible *accessible)
{
EvPageAccessiblePrivate* priv = accessible->priv;
if (priv->links)
return priv->links;
priv->links = g_hash_table_new_full (g_direct_hash,
g_direct_equal,
NULL,
(GDestroyNotify)g_object_unref);
return priv->links;
}
static AtkHyperlink *
ev_page_accessible_get_link (AtkHypertext *hypertext,
gint link_index)
{
GHashTable *links;
EvMappingList *link_mapping;
gint n_links;
EvMapping *mapping;
EvLinkAccessible *atk_link;
EvPageAccessible *self = EV_PAGE_ACCESSIBLE (hypertext);
EvView *view = ev_page_accessible_get_view (self);
if (link_index < 0)
return NULL;
if (!EV_IS_DOCUMENT_LINKS (view->document))
return NULL;
links = ev_page_accessible_get_links (EV_PAGE_ACCESSIBLE (hypertext));
atk_link = g_hash_table_lookup (links, GINT_TO_POINTER (link_index));
if (atk_link)
return atk_hyperlink_impl_get_hyperlink (ATK_HYPERLINK_IMPL (atk_link));
link_mapping = ev_page_cache_get_link_mapping (view->page_cache, self->priv->page);
if (!link_mapping)
return NULL;
n_links = ev_mapping_list_length (link_mapping);
if (link_index > n_links - 1)
return NULL;
mapping = ev_mapping_list_nth (link_mapping, n_links - link_index - 1);
atk_link = ev_link_accessible_new (EV_PAGE_ACCESSIBLE (hypertext),
EV_LINK (mapping->data),
&mapping->area);
g_hash_table_insert (links, GINT_TO_POINTER (link_index), atk_link);
return atk_hyperlink_impl_get_hyperlink (ATK_HYPERLINK_IMPL (atk_link));
}
static gint
ev_page_accessible_get_n_links (AtkHypertext *hypertext)
{
EvMappingList *link_mapping;
EvPageAccessible *self = EV_PAGE_ACCESSIBLE (hypertext);
EvView *view = ev_page_accessible_get_view (self);
if (!EV_IS_DOCUMENT_LINKS (view->document))
return 0;
link_mapping = ev_page_cache_get_link_mapping (view->page_cache,
self->priv->page);
return link_mapping ? ev_mapping_list_length (link_mapping) : 0;
}
static gint
ev_page_accessible_get_link_index (AtkHypertext *hypertext,
gint offset)
{
guint i;
gint n_links = ev_page_accessible_get_n_links (hypertext);
for (i = 0; i < n_links; i++) {
AtkHyperlink *hyperlink;
gint start_index, end_index;
hyperlink = ev_page_accessible_get_link (hypertext, i);
start_index = atk_hyperlink_get_start_index (hyperlink);
end_index = atk_hyperlink_get_end_index (hyperlink);
if (start_index <= offset && end_index >= offset)
return i;
}
return -1;
}
static void
ev_page_accessible_hypertext_iface_init (AtkHypertextIface *iface)
{
iface->get_link = ev_page_accessible_get_link;
iface->get_n_links = ev_page_accessible_get_n_links;
iface->get_link_index = ev_page_accessible_get_link_index;
}
static void
ev_page_accessible_get_extents (AtkComponent *atk_component,
gint *x,
gint *y,
gint *width,
gint *height,
AtkCoordType coord_type)
{
EvPageAccessible *self;
EvView *view;
GdkRectangle page_area;
GtkBorder border;
EvRectangle doc_rect, atk_rect;
self = EV_PAGE_ACCESSIBLE (atk_component);
view = ev_page_accessible_get_view (self);
ev_view_get_page_extents (view, self->priv->page, &page_area, &border);
doc_rect.x1 = page_area.x;
doc_rect.y1 = page_area.y;
doc_rect.x2 = page_area.x + page_area.width;
doc_rect.y2 = page_area.y + page_area.height;
_transform_doc_rect_to_atk_rect (self->priv->view_accessible, self->priv->page, &doc_rect, &atk_rect, coord_type);
*x = atk_rect.x1;
*y = atk_rect.y1;
*width = atk_rect.x2 - atk_rect.x1;
*height = atk_rect.y2 - atk_rect.y1;
}
static void
ev_page_accessible_component_iface_init (AtkComponentIface *iface)
{
iface->get_extents = ev_page_accessible_get_extents;
}
static void
page_cached_cb (EvPageCache *cache,
gint page,
EvPageAccessible *self)
{
if (page == self->priv->page)
ev_page_accessible_initialize_children (self);
}
EvPageAccessible *
ev_page_accessible_new (EvViewAccessible *view_accessible,
gint page)
{
EvPageAccessible *atk_page;
EvView *view;
g_return_val_if_fail (EV_IS_VIEW_ACCESSIBLE (view_accessible), NULL);
g_return_val_if_fail (page >= 0, NULL);
atk_page = g_object_new (EV_TYPE_PAGE_ACCESSIBLE,
"view-accessible", view_accessible,
"page", page,
NULL);
view = ev_page_accessible_get_view (EV_PAGE_ACCESSIBLE (atk_page));
if (ev_page_cache_is_page_cached (view->page_cache, page))
ev_page_accessible_initialize_children (EV_PAGE_ACCESSIBLE (atk_page));
else
g_signal_connect (view->page_cache, "page-cached",
G_CALLBACK (page_cached_cb),
atk_page);
return EV_PAGE_ACCESSIBLE (atk_page);
}
AtkObject *
ev_page_accessible_get_accessible_for_mapping (EvPageAccessible *page_accessible,
EvMapping *mapping)
{
gint i;
ev_page_accessible_initialize_children (page_accessible);
if (!mapping || !page_accessible->priv->children)
return NULL;
for (i = 0; i < page_accessible->priv->children->len; i++) {
AtkObject *child;
child = g_ptr_array_index (page_accessible->priv->children, i);
if (EV_IS_FORM_FIELD_ACCESSIBLE (child) &&
ev_form_field_accessible_get_field (EV_FORM_FIELD_ACCESSIBLE (child)) == mapping->data)
return child;
}
return NULL;
}
void
ev_page_accessible_update_element_state (EvPageAccessible *page_accessible,
EvMapping *mapping)
{
AtkObject *child;
child = ev_page_accessible_get_accessible_for_mapping (page_accessible, mapping);
if (!child)
return;
if (EV_IS_FORM_FIELD_ACCESSIBLE (child))
ev_form_field_accessible_update_state (EV_FORM_FIELD_ACCESSIBLE (child));
}