/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* * Copyright (C) 2013 Intel Corporation * * This library is free software: you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation. * * This library 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 Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see . * * Authors: Tristan Van Berkom */ #include #include "cursor-example.h" #include "cursor-navigator.h" #include "cursor-search.h" #include "cursor-data.h" #include "cursor-slot.h" #define N_SLOTS 10 #define INITIAL_TIMEOUT 600 #define TICK_TIMEOUT 100 #define d(x) typedef enum _TimeoutActivity TimeoutActivity; /* GObjectClass */ static void cursor_example_dispose (GObject *object); /* UI Callbacks */ static gboolean cursor_example_up_button_press (CursorExample *example, GdkEvent *event, GtkButton *button); static gboolean cursor_example_up_button_release (CursorExample *example, GdkEvent *event, GtkButton *button); static gboolean cursor_example_down_button_press (CursorExample *example, GdkEvent *event, GtkButton *button); static gboolean cursor_example_down_button_release (CursorExample *example, GdkEvent *event, GtkButton *button); static void cursor_example_navigator_changed (CursorExample *example, CursorNavigator *navigator); static void cursor_example_sexp_changed (CursorExample *example, GParamSpec *pspec, CursorSearch *search); /* EDS Callbacks */ static void cursor_example_refresh (EBookClientCursor *cursor, CursorExample *example); static void cursor_example_alphabet_changed (EBookClientCursor *cursor, GParamSpec *pspec, CursorExample *example); static void cursor_example_status_changed (EBookClientCursor *cursor, GParamSpec *spec, CursorExample *example); /* Utilities */ static void cursor_example_load_alphabet (CursorExample *example); static gboolean cursor_example_move_cursor (CursorExample *example, EBookCursorOrigin origin, gint count); static gboolean cursor_example_load_page (CursorExample *example, gboolean *full_results); static void cursor_example_update_status (CursorExample *example); static void cursor_example_update_current_index (CursorExample *example, EContact *contact); static void cursor_example_ensure_timeout (CursorExample *example, TimeoutActivity activity); static void cursor_example_cancel_timeout (CursorExample *example); enum _TimeoutActivity { TIMEOUT_NONE = 0, TIMEOUT_UP_INITIAL, TIMEOUT_UP_TICK, TIMEOUT_DOWN_INITIAL, TIMEOUT_DOWN_TICK, }; struct _CursorExamplePrivate { /* Screen widgets */ GtkWidget *browse_up_button; GtkWidget *browse_down_button; GtkWidget *progressbar; GtkWidget *alphabet_label; GtkWidget *slots[N_SLOTS]; CursorNavigator *navigator; /* EDS Resources */ EBookClient *client; EBookClientCursor *cursor; /* Manage the automatic scrolling with button pressed */ guint timeout_id; TimeoutActivity activity; }; G_DEFINE_TYPE_WITH_PRIVATE (CursorExample, cursor_example, GTK_TYPE_WINDOW); /************************************************************************ * GObjectClass * ************************************************************************/ static void cursor_example_class_init (CursorExampleClass *klass) { GObjectClass *object_class; GtkWidgetClass *widget_class; gint i; object_class = G_OBJECT_CLASS (klass); object_class->dispose = cursor_example_dispose; /* Bind to template */ widget_class = GTK_WIDGET_CLASS (klass); gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/evolution/cursor-example/cursor-example.ui"); gtk_widget_class_bind_template_child_private (widget_class, CursorExample, navigator); gtk_widget_class_bind_template_child_private (widget_class, CursorExample, browse_up_button); gtk_widget_class_bind_template_child_private (widget_class, CursorExample, browse_down_button); gtk_widget_class_bind_template_child_private (widget_class, CursorExample, alphabet_label); gtk_widget_class_bind_template_child_private (widget_class, CursorExample, progressbar); for (i = 0; i < N_SLOTS; i++) { gchar *name = g_strdup_printf ("contact_slot_%d", i + 1); gtk_widget_class_bind_template_child_full (widget_class, name, FALSE, 0); g_free (name); } gtk_widget_class_bind_template_callback (widget_class, cursor_example_navigator_changed); gtk_widget_class_bind_template_callback (widget_class, cursor_example_up_button_press); gtk_widget_class_bind_template_callback (widget_class, cursor_example_up_button_release); gtk_widget_class_bind_template_callback (widget_class, cursor_example_down_button_press); gtk_widget_class_bind_template_callback (widget_class, cursor_example_down_button_release); gtk_widget_class_bind_template_callback (widget_class, cursor_example_sexp_changed); } static void cursor_example_init (CursorExample *example) { CursorExamplePrivate *priv; gint i; example->priv = priv = cursor_example_get_instance_private (example); g_type_ensure (CURSOR_TYPE_NAVIGATOR); g_type_ensure (CURSOR_TYPE_SEARCH); gtk_widget_init_template (GTK_WIDGET (example)); for (i = 0; i < N_SLOTS; i++) { gchar *name = g_strdup_printf ("contact_slot_%d", i + 1); priv->slots[i] = (GtkWidget *) gtk_widget_get_template_child (GTK_WIDGET (example), CURSOR_TYPE_EXAMPLE, name); g_free (name); } } static void cursor_example_dispose (GObject *object) { CursorExample *example = CURSOR_EXAMPLE (object); CursorExamplePrivate *priv = example->priv; cursor_example_cancel_timeout (example); g_clear_object (&priv->client); g_clear_object (&priv->cursor); G_OBJECT_CLASS (cursor_example_parent_class)->dispose (object); } /************************************************************************ * UI Callbacks * ************************************************************************/ static gboolean cursor_example_up_button_press (CursorExample *example, GdkEvent *event, GtkButton *button) { d (g_print ("Browse up press\n")); /* Move the cursor backwards by 1 and then refresh the page */ if (cursor_example_move_cursor (example, E_BOOK_CURSOR_ORIGIN_CURRENT, 0 - 1)) cursor_example_load_page (example, NULL); cursor_example_ensure_timeout (example, TIMEOUT_UP_INITIAL); return FALSE; } static gboolean cursor_example_up_button_release (CursorExample *example, GdkEvent *event, GtkButton *button) { d (g_print ("Browse up release\n")); cursor_example_cancel_timeout (example); return FALSE; } static gboolean cursor_example_down_button_press (CursorExample *example, GdkEvent *event, GtkButton *button) { d (g_print ("Browse down press\n")); /* Move the cursor forward by 1 and then refresh the page */ if (cursor_example_move_cursor (example, E_BOOK_CURSOR_ORIGIN_CURRENT, 1)) cursor_example_load_page (example, NULL); cursor_example_ensure_timeout (example, TIMEOUT_DOWN_INITIAL); return FALSE; } static gboolean cursor_example_down_button_release (CursorExample *example, GdkEvent *event, GtkButton *button) { d (g_print ("Browse down released\n")); cursor_example_cancel_timeout (example); return FALSE; } static void cursor_example_navigator_changed (CursorExample *example, CursorNavigator *navigator) { CursorExamplePrivate *priv = example->priv; GError *error = NULL; gint index; gboolean full_results = FALSE; index = cursor_navigator_get_index (priv->navigator); d (g_print ("Alphabet index changed to: %d\n", index)); /* Move to this index */ if (!e_book_client_cursor_set_alphabetic_index_sync (priv->cursor, index, NULL, &error)) { if (g_error_matches (error, E_CLIENT_ERROR, E_CLIENT_ERROR_OUT_OF_SYNC)) { /* Just ignore the error. * * The addressbook locale has recently changed, very * soon we will receive an alphabet change notification * where we will reset the cursor position and reload * the alphabet. */ d (g_print ("Cursor was temporarily out of sync while setting the alphabetic target\n")); } else g_warning ("Failed to move the cursor: %s", error->message); g_clear_error (&error); } /* And load one page full of results starting with this index */ if (!cursor_example_load_page (example, &full_results)) return; /* If we hit the end of the results (less than a full page) then load the last page of results */ if (!full_results) { if (cursor_example_move_cursor (example, E_BOOK_CURSOR_ORIGIN_END, 0 - (N_SLOTS + 1))) { cursor_example_load_page (example, NULL); } } } static void cursor_example_sexp_changed (CursorExample *example, GParamSpec *pspec, CursorSearch *search) { CursorExamplePrivate *priv = example->priv; gboolean full_results = FALSE; GError *error = NULL; const gchar *sexp; sexp = cursor_search_get_sexp (search); d (g_print ("Search expression changed to: '%s'\n", sexp)); /* Set the search expression */ if (!e_book_client_cursor_set_sexp_sync (priv->cursor, sexp, NULL, &error)) { g_warning ("Failed to move the cursor: %s", error->message); g_clear_error (&error); } /* And load one page full of results */ if (!cursor_example_load_page (example, &full_results)) return; /* If we hit the end of the results (less than a full page) then load the last page of results */ if (!full_results) if (cursor_example_move_cursor (example, E_BOOK_CURSOR_ORIGIN_END, 0 - (N_SLOTS + 1))) { cursor_example_load_page (example, NULL); } } /************************************************************************ * EDS Callbacks * ************************************************************************/ static void cursor_example_refresh (EBookClientCursor *cursor, CursorExample *example) { d (g_print ("Cursor refreshed\n")); /* Refresh the page */ if (cursor_example_load_page (example, NULL)) cursor_example_update_status (example); } static void cursor_example_alphabet_changed (EBookClientCursor *cursor, GParamSpec *spec, CursorExample *example) { d (g_print ("Alphabet Changed\n")); cursor_example_load_alphabet (example); /* Get the first page of contacts in the addressbook */ if (cursor_example_move_cursor (example, E_BOOK_CURSOR_ORIGIN_BEGIN, 0)) cursor_example_load_page (example, NULL); } static void cursor_example_status_changed (EBookClientCursor *cursor, GParamSpec *spec, CursorExample *example) { d (g_print ("Status changed\n")); cursor_example_update_status (example); } /************************************************************************ * Utilities * ************************************************************************/ static void cursor_example_load_alphabet (CursorExample *example) { CursorExamplePrivate *priv = example->priv; const gchar *const *alphabet; /* Update the alphabet on the navigator */ alphabet = e_book_client_cursor_get_alphabet (priv->cursor, NULL, NULL, NULL, NULL); cursor_navigator_set_alphabet (priv->navigator, alphabet); /* Reset navigator to the beginning */ g_signal_handlers_block_by_func (priv->navigator, cursor_example_navigator_changed, example); cursor_navigator_set_index (priv->navigator, 0); g_signal_handlers_unblock_by_func (priv->navigator, cursor_example_navigator_changed, example); } static gboolean cursor_example_move_cursor (CursorExample *example, EBookCursorOrigin origin, gint count) { CursorExamplePrivate *priv = example->priv; GError *error = NULL; gint n_results; n_results = e_book_client_cursor_step_sync ( priv->cursor, E_BOOK_CURSOR_STEP_MOVE, origin, count, NULL, /* Result list */ NULL, /* GCancellable */ &error); if (n_results < 0) { if (g_error_matches (error, E_CLIENT_ERROR, E_CLIENT_ERROR_OUT_OF_SYNC)) { /* The addressbook has very recently been modified, * very soon we will receive a "refresh" signal and * automatically reload the current page position. */ d (g_print ("Cursor was temporarily out of sync while moving\n")); } else if (g_error_matches (error, E_CLIENT_ERROR, E_CLIENT_ERROR_QUERY_REFUSED)) { d (g_print ("End of list was reached\n")); } else g_warning ("Failed to move the cursor: %s", error->message); g_clear_error (&error); } return n_results >= 0; } /* Loads a page at the current cursor position, returns * FALSE if there was an error. */ static gboolean cursor_example_load_page (CursorExample *example, gboolean *full_results) { CursorExamplePrivate *priv = example->priv; GError *error = NULL; GSList *results = NULL; gint n_results; /* Fetch N_SLOTS contacts after the current cursor position, * without modifying the current cursor position */ n_results = e_book_client_cursor_step_sync ( priv->cursor, E_BOOK_CURSOR_STEP_FETCH, E_BOOK_CURSOR_ORIGIN_CURRENT, N_SLOTS, &results, NULL, /* GCancellable */ &error); if (n_results < 0) { if (g_error_matches (error, E_CLIENT_ERROR, E_CLIENT_ERROR_OUT_OF_SYNC)) { /* The addressbook has very recently been modified, * very soon we will receive a "refresh" signal and * automatically reload the current page position. */ d (g_print ("Cursor was temporarily out of sync while loading page\n")); } else if (g_error_matches (error, E_CLIENT_ERROR, E_CLIENT_ERROR_QUERY_REFUSED)) { d (g_print ("End of list was reached\n")); } else g_warning ("Failed to move the cursor: %s", error->message); g_clear_error (&error); } else { /* Display the results */ EContact *contact; gint i; /* Fill the page with results for the current cursor position */ for (i = 0; i < N_SLOTS; i++) { contact = g_slist_nth_data (results, i); /* For the first contact, give some visual feedback about where we * are in the list, which alphabet character we're browsing right now. */ if (i == 0 && contact) cursor_example_update_current_index (example, contact); cursor_slot_set_from_contact (CURSOR_SLOT (priv->slots[i]), contact); } } if (full_results) *full_results = (n_results == N_SLOTS); g_slist_free_full (results, (GDestroyNotify) g_object_unref); return n_results >= 0; } static void cursor_example_update_status (CursorExample *example) { CursorExamplePrivate *priv = example->priv; gint total, position; gchar *txt; gboolean up_sensitive; gboolean down_sensitive; gdouble fraction; total = e_book_client_cursor_get_total (priv->cursor); position = e_book_client_cursor_get_position (priv->cursor); /* Set the label showing the cursor position and total contacts */ txt = g_strdup_printf ("Position %d / Total %d", position, total); gtk_progress_bar_set_text (GTK_PROGRESS_BAR (priv->progressbar), txt); g_free (txt); /* Give visual feedback on how far we are into the contact list */ fraction = position * 1.0F / (total - N_SLOTS); gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (priv->progressbar), fraction); /* Update sensitivity of buttons */ if (total <= N_SLOTS) { /* If the amount of contacts is less than the amount of visual slots, * then we cannot browse up and down */ up_sensitive = FALSE; down_sensitive = FALSE; } else { /* The cursor is always pointing directly before * the first contact visible in the view, so if the * cursor is passed the first contact we can rewind. */ up_sensitive = position > 0; /* If more than N_SLOTS contacts remain, then * we can still scroll down */ down_sensitive = position < total - N_SLOTS; } gtk_widget_set_sensitive (priv->browse_up_button, up_sensitive); gtk_widget_set_sensitive (priv->browse_down_button, down_sensitive); } /* This is called when refreshing the window contents with * the first contact shown in the window. */ static void cursor_example_update_current_index (CursorExample *example, EContact *contact) { CursorExamplePrivate *priv = example->priv; const gchar *const *labels; gint index; /* Fetch the alphabetic index for this contact */ index = e_book_client_cursor_get_contact_alphabetic_index (priv->cursor, contact); /* Refresh the current alphabet index indicator. * * The index returned by e_book_client_cursor_get_contact_alphabetic_index() is * a valid position into the array returned by e_book_client_cursor_get_alphabet(). */ labels = e_book_client_cursor_get_alphabet (priv->cursor, NULL, NULL, NULL, NULL); gtk_label_set_text (GTK_LABEL (priv->alphabet_label), labels[index]); /* Update the current scroll position (and avoid reacting to the value change) */ if (contact) { g_signal_handlers_block_by_func (priv->navigator, cursor_example_navigator_changed, example); cursor_navigator_set_index (priv->navigator, index); g_signal_handlers_unblock_by_func (priv->navigator, cursor_example_navigator_changed, example); } } static gboolean cursor_example_timeout (CursorExample *example) { CursorExamplePrivate *priv = example->priv; gboolean can_move; switch (priv->activity) { case TIMEOUT_NONE: break; case TIMEOUT_UP_INITIAL: case TIMEOUT_UP_TICK: /* Move the cursor backwards by 1 and then refresh the page */ if (cursor_example_move_cursor (example, E_BOOK_CURSOR_ORIGIN_CURRENT, 0 - 1)) { cursor_example_load_page (example, NULL); cursor_example_ensure_timeout (example, TIMEOUT_UP_TICK); } else cursor_example_cancel_timeout (example); break; case TIMEOUT_DOWN_INITIAL: case TIMEOUT_DOWN_TICK: /* Avoid scrolling past the end of the list - N_SLOTS */ can_move = (e_book_client_cursor_get_position (priv->cursor) < e_book_client_cursor_get_total (priv->cursor) - N_SLOTS); /* Move the cursor forwards by 1 and then refresh the page */ if (can_move && cursor_example_move_cursor (example, E_BOOK_CURSOR_ORIGIN_CURRENT, 1)) { cursor_example_load_page (example, NULL); cursor_example_ensure_timeout (example, TIMEOUT_DOWN_TICK); } else cursor_example_cancel_timeout (example); break; } return FALSE; } static void cursor_example_ensure_timeout (CursorExample *example, TimeoutActivity activity) { CursorExamplePrivate *priv = example->priv; guint timeout = 0; cursor_example_cancel_timeout (example); if (activity == TIMEOUT_UP_INITIAL || activity == TIMEOUT_DOWN_INITIAL) timeout = INITIAL_TIMEOUT; else timeout = TICK_TIMEOUT; priv->activity = activity; priv->timeout_id = g_timeout_add ( timeout, (GSourceFunc) cursor_example_timeout, example); } static void cursor_example_cancel_timeout (CursorExample *example) { CursorExamplePrivate *priv = example->priv; if (priv->timeout_id) { g_source_remove (priv->timeout_id); priv->timeout_id = 0; } } /************************************************************************ * API * ************************************************************************/ GtkWidget * cursor_example_new (const gchar *vcard_path) { CursorExample *example; CursorExamplePrivate *priv; example = g_object_new (CURSOR_TYPE_EXAMPLE, NULL); priv = example->priv; priv->client = cursor_load_data (vcard_path, &priv->cursor); cursor_example_load_alphabet (example); /* Load the first page of results */ cursor_example_load_page (example, NULL); cursor_example_update_status (example); g_signal_connect (priv->cursor, "refresh", G_CALLBACK (cursor_example_refresh), example); g_signal_connect (priv->cursor, "notify::alphabet", G_CALLBACK (cursor_example_alphabet_changed), example); g_signal_connect (priv->cursor, "notify::total", G_CALLBACK (cursor_example_status_changed), example); g_signal_connect (priv->cursor, "notify::position", G_CALLBACK (cursor_example_status_changed), example); g_message ("Cursor example started in locale: %s", e_book_client_get_locale (priv->client)); return (GtkWidget *) example; }