mirror of
https://gitlab.gnome.org/GNOME/evince
synced 2024-07-04 16:48:55 +00:00
d68515c164
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; | ^~~ } }
1413 lines
42 KiB
C
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));
|
|
}
|