From cd36f3b5e3cd9ee377d3431b51fa3814043f52ef Mon Sep 17 00:00:00 2001
From: Carlos Garcia Campos <carlosgc@gnome.org>
Date: Sat, 8 Jun 2013 11:48:15 +0200
Subject: libview: Initial implementation of caret navigation

Navigation by character, word, next/previous line and beginning/end of
the line using the caret cursor.

The routines to move the cursor don't use GtkTextBuffer to
avoid the duplication of the text for every page.
  - Left/right arrow: Move one character to the left/right.
  - Up/down arrow: Move up/down one line.
  - Ctrl + left/right arrow: Move to the beginning/end of
    the previous/next word.
  - Home/End: Move to the beginning/end of the current line.

https://bugzilla.gnome.org/show_bug.cgi?id=638905

taken from:
https://git.gnome.org/browse/evince/commit/?h=gnome-3-10&id=1dc10fe
---
 libview/ev-view-private.h |   4 +
 libview/ev-view.c         | 457 +++++++++++++++++++++++++++++++++++++++++++++-
 libview/ev-view.h         |   5 +
 3 files changed, 462 insertions(+), 4 deletions(-)

diff --git a/libview/ev-view-private.h b/libview/ev-view-private.h
index c93b0f90..c5180397 100644
--- a/libview/ev-view-private.h
+++ b/libview/ev-view-private.h
@@ -214,6 +214,10 @@ struct _EvView {
 	/* Accessibility */
 	AtkObject *accessible;
 
+	/* Caret navigation */
+	gboolean caret_enabled;
+	gint     cursor_offset;
+
 	/* Gestures */
 	GtkGesture *pan_gesture;
 	GtkGesture *zoom_gesture;
diff --git a/libview/ev-view.c b/libview/ev-view.c
index 650730c0..c3cde707 100644
--- a/libview/ev-view.c
+++ b/libview/ev-view.c
@@ -3230,6 +3230,27 @@ ev_view_synctex_backward_search (EvView *view,
 	return FALSE;
 }
 
+/* Caret navigation */
+void
+ev_view_set_caret_navigation_enabled (EvView   *view,
+				      gboolean enabled)
+{
+	g_return_if_fail (EV_IS_VIEW (view));
+
+	if (view->caret_enabled != enabled) {
+		view->caret_enabled = enabled;
+		gtk_widget_queue_draw (GTK_WIDGET (view));
+	}
+}
+
+gboolean
+ev_view_is_caret_navigation_enabled (EvView *view)
+{
+	g_return_val_if_fail (EV_IS_VIEW (view), FALSE);
+
+	return view->caret_enabled;
+}
+
 /*** GtkWidget implementation ***/
 
 static void
@@ -3698,6 +3719,125 @@ draw_focus (EvView       *view,
 	}
 }
 
+static gboolean
+get_caret_cursor_rect_from_offset (EvView       *view,
+				   gint          offset,
+				   gint          page,
+				   GdkRectangle *rect)
+{
+	EvRectangle *areas = NULL;
+	EvRectangle *doc_rect;
+	guint        n_areas = 0;
+
+	if (!view->caret_enabled)
+		return FALSE;
+
+	/* Disable caret navigation on rotated pages */
+	if (view->rotation != 0)
+		return FALSE;
+
+	if (!view->page_cache)
+		return FALSE;
+
+	ev_page_cache_get_text_layout (view->page_cache, page, &areas, &n_areas);
+	if (!areas)
+		return FALSE;
+
+	if (offset > n_areas)
+		return FALSE;
+
+	doc_rect = areas + offset;
+	if (offset == n_areas ||
+	    ((doc_rect->x1 == doc_rect->x2 || doc_rect->y1 == doc_rect->y2) && offset > 0)) {
+		EvRectangle *prev;
+		EvRectangle  last_rect;
+
+		/* Special characters like \n have an empty bounding box
+		 * and the end of a page doesn't have any bounding box,
+		 * use the size of the previous area.
+		 */
+		prev = areas + offset - 1;
+		last_rect.x1 = prev->x2;
+		last_rect.y1 = prev->y1;
+		last_rect.x2 = prev->x2 + (prev->x2 - prev->x1);
+		last_rect.y2 = prev->y2;
+
+		_ev_view_transform_doc_rect_to_view_rect (view, page, &last_rect, rect);
+
+		return TRUE;
+	}
+
+	_ev_view_transform_doc_rect_to_view_rect (view, page, doc_rect, rect);
+
+	return TRUE;
+}
+
+static void
+get_cursor_color (GtkStyleContext *context,
+		  GdkRGBA         *color)
+{
+	GdkColor *style_color;
+
+	gtk_style_context_get_style (context,
+				     "cursor-color",
+				     &style_color,
+				     NULL);
+
+	if (style_color) {
+		color->red = style_color->red / 65535.0;
+		color->green = style_color->green / 65535.0;
+		color->blue = style_color->blue / 65535.0;
+		color->alpha = 1;
+
+		gdk_color_free (style_color);
+	} else {
+		gtk_style_context_get_color (context, GTK_STATE_FLAG_NORMAL, color);
+	}
+}
+
+/* This is a clone of the deprecated function gtk_draw_insertion_cursor. */
+static void
+render_cursor (GtkWidget    *widget,
+	       cairo_t      *cr,
+	       GdkRectangle *rect)
+{
+
+	GtkStyleContext *context;
+	GdkRGBA cursor_color;
+	gfloat cursor_aspect_ratio;
+	gint stem_width;
+
+	context = gtk_widget_get_style_context (widget);
+	get_cursor_color (context, &cursor_color);
+
+	gtk_style_context_get_style (context,
+				     "cursor-aspect-ratio", &cursor_aspect_ratio,
+				     NULL);
+
+	stem_width = rect->height * cursor_aspect_ratio + 1;
+
+	cairo_save (cr);
+	gdk_cairo_set_source_rgba (cr, &cursor_color);
+	cairo_rectangle (cr, rect->x - (stem_width / 2), rect->y, stem_width, rect->height);
+	cairo_fill (cr);
+	cairo_restore (cr);
+}
+
+static void
+draw_caret_cursor (EvView  *view,
+		   cairo_t *cr)
+{
+	GdkRectangle view_rect;
+
+	if (!get_caret_cursor_rect_from_offset (view, view->cursor_offset, view->current_page, &view_rect))
+		return;
+
+	view_rect.x = view_rect.x - view->scroll_x;
+	view_rect.y = view_rect.y - view->scroll_y;
+
+	render_cursor (GTK_WIDGET (view), cr, &view_rect);
+}
+
 static gboolean
 ev_view_draw (GtkWidget      *widget,
 	      cairo_t *cr)
@@ -3739,6 +3879,8 @@ ev_view_draw (GtkWidget      *widget,
 
 		draw_one_page (view, i, cr, &page_area, &border, area, &page_ready);
 
+		if (page_ready && view->caret_enabled && view->current_page == i)
+			draw_caret_cursor (view, cr);
 		if (page_ready && view->find_pages && view->highlight_find_results)
 			highlight_find_results (view, cr, i);
 		if (page_ready && EV_IS_DOCUMENT_ANNOTATIONS (view->document))
@@ -4550,6 +4692,293 @@ ev_view_forward_key_event_to_focused_child (EvView      *view,
 	return handled;
 }
 
+static gboolean
+cursor_backward_char (EvView *view)
+{
+	EvRectangle *areas = NULL;
+	guint        n_areas = 0;
+
+	if (!view->page_cache)
+		return FALSE;
+
+	ev_page_cache_get_text_layout (view->page_cache, view->current_page, &areas, &n_areas);
+	if (!areas)
+		return FALSE;
+
+	if (view->cursor_offset == 0)
+		return ev_view_previous_page (view);
+
+	view->cursor_offset--;
+
+	return TRUE;
+}
+
+static gboolean
+cursor_forward_char (EvView *view)
+{
+	EvRectangle *areas = NULL;
+	guint        n_areas = 0;
+
+	if (!view->page_cache)
+		return FALSE;
+
+	ev_page_cache_get_text_layout (view->page_cache, view->current_page, &areas, &n_areas);
+	if (!areas)
+		return FALSE;
+
+	if (view->cursor_offset >= n_areas)
+		return ev_view_next_page (view);
+
+	view->cursor_offset++;
+
+	return TRUE;
+}
+
+static gboolean
+cursor_backward_word_start (EvView *view)
+{
+	EvRectangle *areas = NULL;
+	guint        n_areas = 0;
+	const gchar *page_text;
+	gint         i, j;
+
+	if (!view->page_cache)
+		return FALSE;
+
+	ev_page_cache_get_text_layout (view->page_cache, view->current_page, &areas, &n_areas);
+	if (!areas)
+		return FALSE;
+
+	page_text = ev_page_cache_get_text (view->page_cache, view->current_page);
+	if (!page_text)
+		return FALSE;
+
+	/* Skip blanks and new lines. */
+	/* FIXME: Text is not ASCII but utf8, use pango for word breaking. */
+	for (i = view->cursor_offset - 1; i >= 0 && g_ascii_isspace (page_text[i]); i--);
+
+	if (i <= 0) {
+		if (ev_view_previous_page (view))
+			return cursor_backward_word_start (view);
+		return FALSE;
+	}
+
+	/* Move to the beginning of the word */
+	/* FIXME: Text is not ASCII but utf8, use pango for word breaking. */
+	for (j = i; j >= 0 && !g_ascii_isspace (page_text[j]); j--);
+
+	if (j <= 0)
+		view->cursor_offset = 0;
+	else
+		view->cursor_offset = j + 1;
+
+	return TRUE;
+}
+
+static gboolean
+cursor_forward_word_end (EvView *view)
+{
+	EvRectangle *areas = NULL;
+	guint        n_areas = 0;
+	const gchar *page_text;
+	gint         i, j;
+
+	if (!view->page_cache)
+		return FALSE;
+
+	ev_page_cache_get_text_layout (view->page_cache, view->current_page, &areas, &n_areas);
+	if (!areas)
+		return FALSE;
+
+	page_text = ev_page_cache_get_text (view->page_cache, view->current_page);
+	if (!page_text)
+		return FALSE;
+
+	/* Skip blanks and new lines. */
+	/* FIXME: Text is not ASCII but utf8, use pango for word breaking. */
+	for (i = view->cursor_offset; i < n_areas && g_ascii_isspace (page_text[i]); i++);
+
+	if (i >= n_areas) {
+		if (ev_view_next_page (view))
+			return cursor_forward_word_end (view);
+		return FALSE;
+	}
+
+	/* Move to the end of the word. */
+	/* FIXME: Text is not ASCII but utf8, use pango for word breaking. */
+	for (j = i; j < (gint)n_areas && !g_ascii_isspace (page_text[j]); j++);
+
+	if (j >= n_areas)
+		view->cursor_offset = n_areas;
+	else
+		view->cursor_offset = j;
+
+	return TRUE;
+}
+
+static gboolean
+cursor_go_to_line_start (EvView *view)
+{
+	EvRectangle *areas = NULL;
+	guint        n_areas = 0;
+	const gchar *page_text;
+	gint         i;
+
+	if (!view->page_cache)
+		return FALSE;
+
+	ev_page_cache_get_text_layout (view->page_cache, view->current_page, &areas, &n_areas);
+	if (!areas)
+		return FALSE;
+
+	page_text = ev_page_cache_get_text (view->page_cache, view->current_page);
+	if (!page_text)
+		return FALSE;
+
+	/* FIXME: Text is not ASCII but utf8, use pango for line breaking. */
+	for (i = view->cursor_offset - 1; i >= 0 && page_text[i] != '\n'; i--);
+
+	if (i <= 0)
+		view->cursor_offset = 0;
+	else
+		view->cursor_offset = i + 1;
+
+	return TRUE;
+}
+
+static gboolean
+cursor_backward_line (EvView *view)
+{
+	/* FIXME: Keep the line offset when moving between lines */
+	if (!cursor_go_to_line_start (view))
+		return FALSE;
+
+	if (view->cursor_offset == 0)
+		ev_view_previous_page (view);
+	else
+		view->cursor_offset--;
+
+	return TRUE;
+}
+
+static gboolean
+cursor_go_to_line_end (EvView *view)
+{
+	EvRectangle *areas = NULL;
+	guint        n_areas = 0;
+	const gchar *page_text;
+	gint         i;
+
+	if (!view->page_cache)
+		return FALSE;
+
+	ev_page_cache_get_text_layout (view->page_cache, view->current_page, &areas, &n_areas);
+	if (!areas)
+		return FALSE;
+
+	page_text = ev_page_cache_get_text (view->page_cache, view->current_page);
+	if (!page_text)
+		return FALSE;
+
+	/* FIXME: Text is not ASCII but utf8, use pango for line breaking. */
+	for (i = view->cursor_offset; i < (gint)n_areas && page_text[i] != '\n'; i++);
+
+	if (i >= n_areas)
+		view->cursor_offset = n_areas;
+	else
+		view->cursor_offset = i;
+
+	return TRUE;
+}
+
+static gboolean
+cursor_forward_line (EvView *view)
+{
+	EvRectangle *areas = NULL;
+	guint        n_areas = 0;
+
+	/* FIXME: Keep the line offset when moving between lines */
+	if (!cursor_go_to_line_end (view))
+		return FALSE;
+
+	ev_page_cache_get_text_layout (view->page_cache, view->current_page, &areas, &n_areas);
+	if (!areas)
+		return FALSE;
+
+	if (view->cursor_offset == n_areas)
+		ev_view_next_page (view);
+	else
+		view->cursor_offset++;
+
+	return TRUE;
+}
+
+static gboolean
+cursor_go_to_page_start (EvView *view)
+{
+	view->cursor_offset = 0;
+
+	return TRUE;
+}
+
+static gboolean
+cursor_go_to_page_end (EvView *view)
+{
+	EvRectangle *areas = NULL;
+	guint        n_areas = 0;
+
+	if (!view->page_cache)
+		return FALSE;
+
+	ev_page_cache_get_text_layout (view->page_cache, view->current_page, &areas, &n_areas);
+	if (!areas)
+		return FALSE;
+
+	view->cursor_offset = n_areas;
+
+	return TRUE;
+}
+
+static gboolean
+caret_key_press_event (EvView      *view,
+		       GdkEventKey *event)
+{
+	/* Disable caret navigation on rotated pages */
+	if (view->rotation != 0)
+		return FALSE;
+
+	switch (event->keyval) {
+	case GDK_KEY_Left:
+		if (event->state & GDK_CONTROL_MASK)
+			cursor_backward_word_start (view);
+		else
+			cursor_backward_char (view);
+		break;
+	case GDK_KEY_Right:
+		if (event->state & GDK_CONTROL_MASK)
+			cursor_forward_word_end (view);
+		else
+			cursor_forward_char (view);
+		break;
+	case GDK_KEY_Up:
+		cursor_backward_line (view);
+		break;
+	case GDK_KEY_Down:
+		cursor_forward_line (view);
+		break;
+	case GDK_KEY_Home:
+		cursor_go_to_line_start (view);
+		break;
+	case GDK_KEY_End:
+		cursor_go_to_line_end (view);
+		break;
+	default:
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
 static gboolean
 ev_view_key_press_event (GtkWidget   *widget,
 			 GdkEventKey *event)
@@ -4559,6 +4988,19 @@ ev_view_key_press_event (GtkWidget   *widget,
 	if (!view->document)
 		return FALSE;
 
+	if (view->caret_enabled && caret_key_press_event (view, event)) {
+		GdkRectangle view_rect;
+
+		/* scroll to view the text caret */
+		if (!get_caret_cursor_rect_from_offset (view, view->cursor_offset, view->current_page, &view_rect))
+			return TRUE;
+
+		ensure_rectangle_is_visible (view, &view_rect);
+		gtk_widget_queue_draw (GTK_WIDGET (view));
+
+		return TRUE;
+	}
+
 	if (!gtk_widget_has_focus (widget))
 		return ev_view_forward_key_event_to_focused_child (view, event);
 
@@ -5406,6 +5848,7 @@ ev_view_init (EvView *view)
 	view->pending_scroll = SCROLL_TO_KEEP_POSITION;
 	view->jump_to_find_result = TRUE;
 	view->highlight_find_results = FALSE;
+	view->caret_enabled = FALSE;
 	view->zoom_center_x = -1;
 	view->zoom_center_y = -1;
 
@@ -7003,13 +7446,16 @@ ev_view_next_page (EvView *view)
 
 	if (page < n_pages) {
 		ev_document_model_set_page (view->model, page);
-		return TRUE;
 	} else if (dual_page && page == n_pages) {
 		ev_document_model_set_page (view->model, page - 1);
-		return TRUE;
 	} else {
 		return FALSE;
 	}
+
+	if (view->caret_enabled)
+		cursor_go_to_page_start (view);
+
+	return TRUE;
 }
 
 gboolean
@@ -7033,13 +7479,16 @@ ev_view_previous_page (EvView *view)
 
 	if (page >= 0) {
 		ev_document_model_set_page (view->model, page);
-		return TRUE;
 	} else if (dual_page && page == -1) {
 		ev_document_model_set_page (view->model, 0);
-		return TRUE;
 	} else {	
 		return FALSE;
 	}
+
+	if (view->caret_enabled)
+		cursor_go_to_page_end (view);
+
+	return TRUE;
 }
 		
 /**
diff --git a/libview/ev-view.h b/libview/ev-view.h
index d46cfcef..fdce44c7 100644
--- a/libview/ev-view.h
+++ b/libview/ev-view.h
@@ -117,6 +117,11 @@ void           ev_view_remove_annotation     (EvView          *view,
 
 /*For epub*/
 void           ev_view_disconnect_handlers   (EvView          *view);
+
+/* Caret navigation */
+gboolean       ev_view_is_caret_navigation_enabled  (EvView  *view);
+void           ev_view_set_caret_navigation_enabled (EvView  *view,
+                                                     gboolean enabled);
 G_END_DECLS
 
 #endif /* __EV_VIEW_H__ */
-- 
cgit v1.2.1