aboutsummaryrefslogtreecommitdiff
path: root/src/timezone-completion.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/timezone-completion.c')
-rw-r--r--src/timezone-completion.c424
1 files changed, 424 insertions, 0 deletions
diff --git a/src/timezone-completion.c b/src/timezone-completion.c
new file mode 100644
index 0000000..f570c33
--- /dev/null
+++ b/src/timezone-completion.c
@@ -0,0 +1,424 @@
+/* -*- Mode: C; coding: utf-8; indent-tabs-mode: nil; tab-width: 2 -*-
+
+Copyright 2011 Canonical Ltd.
+
+Authors:
+ Michael Terry <michael.terry@canonical.com>
+
+This program is free software: you can redistribute it and/or modify it
+under the terms of the GNU General Public License version 3, as published
+by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranties of
+MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <json-glib/json-glib.h>
+#include <gdk/gdk.h>
+#include <glib/gi18n.h>
+#include "timezone-completion.h"
+#include "tz.h"
+
+enum {
+ LAST_SIGNAL
+};
+
+/* static guint signals[LAST_SIGNAL] = { }; */
+
+typedef struct _TimezoneCompletionPrivate TimezoneCompletionPrivate;
+struct _TimezoneCompletionPrivate
+{
+ GtkTreeModel * initial_model;
+ GtkEntry * entry;
+ guint queued_request;
+ guint changed_id;
+ GCancellable * cancel;
+ gchar * request_text;
+ GHashTable * request_table;
+};
+
+#define TIMEZONE_COMPLETION_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE((o), TIMEZONE_COMPLETION_TYPE, TimezoneCompletionPrivate))
+
+#define GEONAME_URL "http://geoname-lookup.ubuntu.com/?query=%s&release=%s"
+
+/* Prototypes */
+static void timezone_completion_class_init (TimezoneCompletionClass *klass);
+static void timezone_completion_init (TimezoneCompletion *self);
+static void timezone_completion_dispose (GObject *object);
+static void timezone_completion_finalize (GObject *object);
+
+G_DEFINE_TYPE (TimezoneCompletion, timezone_completion, GTK_TYPE_ENTRY_COMPLETION);
+
+static void
+save_and_use_model (TimezoneCompletion * completion, GtkTreeModel * model)
+{
+ TimezoneCompletionPrivate * priv = TIMEZONE_COMPLETION_GET_PRIVATE(completion);
+
+ g_hash_table_insert (priv->request_table, g_strdup (priv->request_text), g_object_ref (model));
+ gtk_entry_completion_set_model (GTK_ENTRY_COMPLETION (completion), model);
+ gtk_entry_completion_complete (GTK_ENTRY_COMPLETION (completion));
+}
+
+static void
+json_parse_ready (GObject *object, GAsyncResult *res, gpointer user_data)
+{
+ TimezoneCompletion * completion = TIMEZONE_COMPLETION (user_data);
+ TimezoneCompletionPrivate * priv = TIMEZONE_COMPLETION_GET_PRIVATE(completion);
+ GError * error = NULL;
+
+ json_parser_load_from_stream_finish (JSON_PARSER (object), res, &error);
+
+ if (priv->cancel && (error == NULL || error->code != G_IO_ERROR_CANCELLED)) {
+ g_cancellable_reset (priv->cancel);
+ }
+
+ if (error != NULL) {
+ g_warning ("Could not parse geoname JSON data: %s", error->message);
+ g_error_free (error);
+ save_and_use_model (completion, priv->initial_model);
+ return;
+ }
+
+ GtkListStore * store = gtk_list_store_new (TIMEZONE_COMPLETION_LAST,
+ G_TYPE_STRING,
+ G_TYPE_STRING,
+ G_TYPE_STRING,
+ G_TYPE_STRING,
+ G_TYPE_STRING,
+ G_TYPE_STRING);
+
+ JsonReader * reader = json_reader_new (json_parser_get_root (JSON_PARSER (object)));
+
+ if (!json_reader_is_array (reader))
+ return;
+
+ gint i, count = json_reader_count_elements (reader);
+ for (i = 0; i < count; ++i) {
+ if (!json_reader_read_element (reader, i))
+ continue;
+
+ if (json_reader_is_object (reader)) {
+ const gchar * name = NULL;
+ const gchar * admin1 = NULL;
+ const gchar * country = NULL;
+ const gchar * longitude = NULL;
+ const gchar * latitude = NULL;
+ if (json_reader_read_member (reader, "name")) {
+ name = json_reader_get_string_value (reader);
+ json_reader_end_member (reader);
+ }
+ if (json_reader_read_member (reader, "admin1")) {
+ admin1 = json_reader_get_string_value (reader);
+ json_reader_end_member (reader);
+ }
+ if (json_reader_read_member (reader, "country")) {
+ country = json_reader_get_string_value (reader);
+ json_reader_end_member (reader);
+ }
+ if (json_reader_read_member (reader, "longitude")) {
+ longitude = json_reader_get_string_value (reader);
+ json_reader_end_member (reader);
+ }
+ if (json_reader_read_member (reader, "latitude")) {
+ latitude = json_reader_get_string_value (reader);
+ json_reader_end_member (reader);
+ }
+
+ GtkTreeIter iter;
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter,
+ TIMEZONE_COMPLETION_ZONE, NULL,
+ TIMEZONE_COMPLETION_NAME, name,
+ TIMEZONE_COMPLETION_ADMIN1, admin1,
+ TIMEZONE_COMPLETION_COUNTRY, country,
+ TIMEZONE_COMPLETION_LONGITUDE, longitude,
+ TIMEZONE_COMPLETION_LATITUDE, latitude,
+ -1);
+ }
+
+ json_reader_end_element (reader);
+ }
+
+ save_and_use_model (completion, GTK_TREE_MODEL (store));
+ g_object_unref (G_OBJECT (store));
+}
+
+static void
+geonames_data_ready (GObject *object, GAsyncResult *res, gpointer user_data)
+{
+ TimezoneCompletion * completion = TIMEZONE_COMPLETION (user_data);
+ TimezoneCompletionPrivate * priv = TIMEZONE_COMPLETION_GET_PRIVATE (completion);
+ GError * error = NULL;
+ GFileInputStream * stream;
+
+ stream = g_file_read_finish (G_FILE (object), res, &error);
+
+ if (priv->cancel && (error == NULL || error->code != G_IO_ERROR_CANCELLED)) {
+ g_cancellable_reset (priv->cancel);
+ }
+
+ if (error != NULL) {
+ g_warning ("Could not connect to geoname lookup server: %s", error->message);
+ g_error_free (error);
+ save_and_use_model (completion, priv->initial_model);
+ return;
+ }
+
+ JsonParser * parser = json_parser_new ();
+ json_parser_load_from_stream_async (parser, G_INPUT_STREAM (stream), priv->cancel,
+ json_parse_ready, user_data);
+}
+
+static gboolean
+request_zones (TimezoneCompletion * completion)
+{
+ TimezoneCompletionPrivate * priv = TIMEZONE_COMPLETION_GET_PRIVATE (completion);
+
+ priv->queued_request = 0;
+
+ if (priv->entry == NULL) {
+ return FALSE;
+ }
+
+ const gchar * text = gtk_entry_get_text (priv->entry);
+
+ gpointer data;
+ if (g_hash_table_lookup_extended (priv->request_table, text, NULL, &data)) {
+ gtk_entry_completion_set_model (GTK_ENTRY_COMPLETION (completion), GTK_TREE_MODEL (data));
+ gtk_entry_completion_complete (GTK_ENTRY_COMPLETION (completion));
+ return FALSE;
+ }
+
+ /* Cancel any ongoing request */
+ if (priv->cancel) {
+ g_cancellable_cancel (priv->cancel);
+ g_cancellable_reset (priv->cancel);
+ }
+ g_free (priv->request_text);
+
+ priv->request_text = g_strdup (text);
+
+ gchar * escaped = g_uri_escape_string (text, NULL, FALSE);
+ gchar * url = g_strdup_printf (GEONAME_URL, escaped, "11.04"); // FIXME: don't hardcode
+
+ GFile * file = g_file_new_for_uri (url);
+ g_file_read_async (file, G_PRIORITY_DEFAULT, priv->cancel,
+ geonames_data_ready, completion);
+
+ return FALSE;
+}
+
+static void
+entry_changed (GtkEntry * entry, TimezoneCompletion * completion)
+{
+ TimezoneCompletionPrivate * priv = TIMEZONE_COMPLETION_GET_PRIVATE (completion);
+
+ if (priv->queued_request) {
+ g_source_remove (priv->queued_request);
+ }
+ priv->queued_request = g_timeout_add (300, (GSourceFunc)request_zones, completion);
+}
+
+void
+timezone_completion_watch_entry (TimezoneCompletion * completion, GtkEntry * entry)
+{
+ TimezoneCompletionPrivate * priv = TIMEZONE_COMPLETION_GET_PRIVATE (completion);
+
+ if (priv->entry) {
+ g_source_remove (priv->changed_id);
+ g_object_remove_weak_pointer (G_OBJECT (priv->entry), (gpointer *)&priv->entry);
+ }
+
+ guint id = g_signal_connect (entry, "changed", G_CALLBACK (entry_changed), completion);
+ priv->changed_id = id;
+
+ priv->entry = entry;
+ g_object_add_weak_pointer (G_OBJECT (entry), (gpointer *)&priv->entry);
+}
+
+static GtkListStore *
+get_initial_model (void)
+{
+ TzDB * db = tz_load_db ();
+ GPtrArray * locations = tz_get_locations (db);
+
+ GtkListStore * store = gtk_list_store_new (TIMEZONE_COMPLETION_LAST,
+ G_TYPE_STRING,
+ G_TYPE_STRING,
+ G_TYPE_STRING,
+ G_TYPE_STRING,
+ G_TYPE_STRING,
+ G_TYPE_STRING);
+
+ gint i;
+ for (i = 0; i < locations->len; ++i) {
+ TzLocation * loc = g_ptr_array_index (locations, i);
+ GtkTreeIter iter;
+ gtk_list_store_append (store, &iter);
+
+ /* FIXME: need something better than below for non-English locales */
+ const gchar * last_bit = ((const gchar *)strrchr (loc->zone, '/')) + 1;
+ if (last_bit == NULL)
+ last_bit = loc->zone;
+ gchar * name = g_strdup (last_bit);
+ gchar * underscore;
+ while ((underscore = strchr (name, '_'))) {
+ *underscore = ' ';
+ }
+
+ gtk_list_store_set (store, &iter,
+ TIMEZONE_COMPLETION_ZONE, loc->zone,
+ TIMEZONE_COMPLETION_NAME, name,
+ TIMEZONE_COMPLETION_COUNTRY, loc->country,
+ -1);
+
+ g_free (name);
+ }
+
+ tz_db_free (db);
+ return store;
+}
+
+static void
+data_func (GtkCellLayout *cell_layout, GtkCellRenderer *cell,
+ GtkTreeModel *tree_model, GtkTreeIter *iter, gpointer user_data)
+{
+ GValue name_val = {0}, admin1_val = {0}, country_val = {0};
+ const gchar * name, * admin1, * country;
+
+ gtk_tree_model_get_value (GTK_TREE_MODEL (tree_model), iter, TIMEZONE_COMPLETION_NAME, &name_val);
+ gtk_tree_model_get_value (GTK_TREE_MODEL (tree_model), iter, TIMEZONE_COMPLETION_ADMIN1, &admin1_val);
+ gtk_tree_model_get_value (GTK_TREE_MODEL (tree_model), iter, TIMEZONE_COMPLETION_COUNTRY, &country_val);
+
+ name = g_value_get_string (&name_val);
+ admin1 = g_value_get_string (&admin1_val);
+ country = g_value_get_string (&country_val);
+
+ gchar * user_name;
+ if (admin1 == NULL || admin1[0] == 0) {
+ user_name = g_strdup_printf ("%s <small>(%s)</small>", name, country);
+ } else {
+ user_name = g_strdup_printf ("%s <small>(%s, %s)</small>", name, admin1, country);
+ }
+
+ g_object_set (G_OBJECT (cell), "markup", user_name, NULL);
+
+ g_value_unset (&name_val);
+ g_value_unset (&admin1_val);
+ g_value_unset (&country_val);
+}
+
+static gboolean
+match_func (GtkEntryCompletion *completion, const gchar *key,
+ GtkTreeIter *iter, gpointer user_data)
+{
+ // geonames does the work for us
+ return TRUE;
+}
+
+static void
+timezone_completion_class_init (TimezoneCompletionClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ g_type_class_add_private (klass, sizeof (TimezoneCompletionPrivate));
+
+ object_class->dispose = timezone_completion_dispose;
+ object_class->finalize = timezone_completion_finalize;
+
+ return;
+}
+
+static void
+timezone_completion_init (TimezoneCompletion * self)
+{
+ TimezoneCompletionPrivate * priv = TIMEZONE_COMPLETION_GET_PRIVATE (self);
+
+ priv->initial_model = GTK_TREE_MODEL (get_initial_model ());
+
+ gtk_entry_completion_set_match_func (GTK_ENTRY_COMPLETION (self), match_func, NULL, NULL);
+ g_object_set (G_OBJECT (self),
+ "text-column", TIMEZONE_COMPLETION_NAME,
+ "popup-set-width", FALSE,
+ NULL);
+
+ priv->cancel = g_cancellable_new ();
+
+ priv->request_table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
+
+ GtkCellRenderer * cell = gtk_cell_renderer_text_new ();
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (self), cell, TRUE);
+ gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self), cell, data_func, NULL, NULL);
+
+ return;
+}
+
+static void
+timezone_completion_dispose (GObject * object)
+{
+ G_OBJECT_CLASS (timezone_completion_parent_class)->dispose (object);
+
+ TimezoneCompletion * completion = TIMEZONE_COMPLETION (object);
+ TimezoneCompletionPrivate * priv = TIMEZONE_COMPLETION_GET_PRIVATE (completion);
+
+ if (priv->changed_id) {
+ g_source_remove (priv->changed_id);
+ priv->changed_id = 0;
+ }
+
+ if (priv->entry != NULL) {
+ g_object_remove_weak_pointer (G_OBJECT (priv->entry), (gpointer *)&priv->entry);
+ }
+
+ if (priv->initial_model != NULL) {
+ g_object_unref (G_OBJECT (priv->initial_model));
+ priv->initial_model = NULL;
+ }
+
+ if (priv->queued_request) {
+ g_source_remove (priv->queued_request);
+ priv->queued_request = 0;
+ }
+
+ if (priv->cancel != NULL) {
+ g_cancellable_cancel (priv->cancel);
+ g_object_unref (priv->cancel);
+ priv->cancel = NULL;
+ }
+
+ if (priv->request_text != NULL) {
+ g_free (priv->request_text);
+ priv->request_text = NULL;
+ }
+
+ if (priv->request_table != NULL) {
+ g_hash_table_destroy (priv->request_table);
+ priv->request_table = NULL;
+ }
+
+ return;
+}
+
+static void
+timezone_completion_finalize (GObject * object)
+{
+ G_OBJECT_CLASS (timezone_completion_parent_class)->finalize (object);
+ return;
+}
+
+TimezoneCompletion *
+timezone_completion_new ()
+{
+ TimezoneCompletion * self = g_object_new (TIMEZONE_COMPLETION_TYPE, NULL);
+ return self;
+}
+