1 /* -*- Mode: C; coding: utf-8; indent-tabs-mode: nil; tab-width: 2 -*-
2 
3 Copyright 2011 Canonical Ltd.
4 
5 Authors:
6     Michael Terry <michael.terry@canonical.com>
7 
8 This program is free software: you can redistribute it and/or modify it
9 under the terms of the GNU General Public License version 3, as published
10 by the Free Software Foundation.
11 
12 This program is distributed in the hope that it will be useful, but
13 WITHOUT ANY WARRANTY; without even the implied warranties of
14 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15 PURPOSE.  See the GNU General Public License for more details.
16 
17 You should have received a copy of the GNU General Public License along
18 with this program.  If not, see <http://www.gnu.org/licenses/>.
19 */
20 
21 #ifdef HAVE_CONFIG_H
22 #include "config.h"
23 #endif
24 
25 #include <json-glib/json-glib.h>
26 #include <gdk/gdk.h>
27 #include <gdk/gdkkeysyms.h>
28 #include <glib/gi18n.h>
29 #include <libsoup/soup.h>
30 #include "timezone-completion.h"
31 #include "tz.h"
32 
33 enum {
34   LAST_SIGNAL
35 };
36 
37 /* static guint signals[LAST_SIGNAL] = { }; */
38 
39 struct _CcTimezoneCompletionPrivate
40 {
41   GtkTreeModel * initial_model;
42   GtkEntry *     entry;
43   guint          queued_request;
44   guint          changed_id;
45   guint          keypress_id;
46   GCancellable * cancel;
47   gchar *        request_text;
48   GHashTable *   request_table;
49   SoupSession *  soup_session;
50 };
51 
52 #define GEONAME_URL "http://geoname-lookup.ubuntu.com/?query=%s&release=%s&lang=%s"
53 
54 /* Prototypes */
55 static void cc_timezone_completion_class_init (CcTimezoneCompletionClass *klass);
56 static void cc_timezone_completion_init       (CcTimezoneCompletion *self);
57 static void cc_timezone_completion_dispose    (GObject *object);
58 static void cc_timezone_completion_finalize   (GObject *object);
59 
60 G_DEFINE_TYPE (CcTimezoneCompletion, cc_timezone_completion, GTK_TYPE_ENTRY_COMPLETION);
61 
62 static gboolean
match_func(GtkEntryCompletion * completion,const gchar * key,GtkTreeIter * iter,gpointer user_data)63 match_func (GtkEntryCompletion *completion, const gchar *key,
64             GtkTreeIter *iter, gpointer user_data)
65 {
66   // geonames does the work for us
67   return TRUE;
68 }
69 
70 static void
save_and_use_model(CcTimezoneCompletion * completion,GtkTreeModel * model)71 save_and_use_model (CcTimezoneCompletion * completion, GtkTreeModel * model)
72 {
73   CcTimezoneCompletionPrivate * priv = completion->priv;
74 
75   g_hash_table_insert (priv->request_table, g_strdup (priv->request_text), g_object_ref_sink (model));
76 
77   if (model == priv->initial_model)
78     gtk_entry_completion_set_match_func (GTK_ENTRY_COMPLETION (completion), NULL, NULL, NULL);
79   else
80     gtk_entry_completion_set_match_func (GTK_ENTRY_COMPLETION (completion), match_func, NULL, NULL);
81 
82   gtk_entry_completion_set_model (GTK_ENTRY_COMPLETION (completion), model);
83 
84   if (priv->entry != NULL) {
85     gtk_entry_completion_complete (GTK_ENTRY_COMPLETION (completion));
86 
87     /* By this time, the changed signal has come and gone.  We didn't give a
88        model to use, so no popup appeared for user.  Poke the entry again to show
89        popup in 300ms. */
90     g_signal_emit_by_name (priv->entry, "changed");
91   }
92 }
93 
94 static gint
sort_zone(GtkTreeModel * model,GtkTreeIter * a,GtkTreeIter * b,gpointer user_data)95 sort_zone (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b,
96            gpointer user_data)
97 {
98   /* Anything that has text as a prefix goes first, in mostly sorted order.
99      Then everything else goes after, in mostly sorted order. */
100   const gchar *casefolded_text = (const gchar *)user_data;
101 
102   const gchar *namea = NULL, *nameb = NULL;
103   gtk_tree_model_get (model, a, CC_TIMEZONE_COMPLETION_NAME, &namea, -1);
104   gtk_tree_model_get (model, b, CC_TIMEZONE_COMPLETION_NAME, &nameb, -1);
105 
106   gchar *casefolded_namea = NULL, *casefolded_nameb = NULL;
107   casefolded_namea = g_utf8_casefold (namea, -1);
108   casefolded_nameb = g_utf8_casefold (nameb, -1);
109 
110   gboolean amatches = FALSE, bmatches = FALSE;
111   amatches = strncmp (casefolded_text, casefolded_namea, strlen(casefolded_text)) == 0;
112   bmatches = strncmp (casefolded_text, casefolded_nameb, strlen(casefolded_text)) == 0;
113 
114   gint rv;
115   if (amatches && !bmatches)
116     rv = -1;
117   else if (bmatches && !amatches)
118     rv = 1;
119   else
120     rv = g_utf8_collate (casefolded_namea, casefolded_nameb);
121 
122   g_free (casefolded_namea);
123   g_free (casefolded_nameb);
124   return rv;
125 }
126 
127 static void
json_parse_ready(GObject * object,GAsyncResult * res,gpointer user_data)128 json_parse_ready (GObject *object, GAsyncResult *res, gpointer user_data)
129 {
130   CcTimezoneCompletion * completion = CC_TIMEZONE_COMPLETION (user_data);
131   CcTimezoneCompletionPrivate * priv = completion->priv;
132   GError * error = NULL;
133   const gchar * prev_name = NULL;
134   const gchar * prev_admin1 = NULL;
135   const gchar * prev_country = NULL;
136 
137   json_parser_load_from_stream_finish (JSON_PARSER (object), res, &error);
138 
139   if (error != NULL)
140     {
141       if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
142         save_and_use_model (completion, priv->initial_model);
143       g_warning ("Could not parse geoname JSON data: %s", error->message);
144       g_error_free (error);
145       return;
146     }
147 
148   GtkListStore * store = gtk_list_store_new (CC_TIMEZONE_COMPLETION_LAST,
149                                              G_TYPE_STRING,
150                                              G_TYPE_STRING,
151                                              G_TYPE_STRING,
152                                              G_TYPE_STRING,
153                                              G_TYPE_STRING,
154                                              G_TYPE_STRING);
155 
156   JsonReader * reader = json_reader_new (json_parser_get_root (JSON_PARSER (object)));
157 
158   if (!json_reader_is_array (reader))
159     {
160       g_warning ("Could not parse geoname JSON data");
161       save_and_use_model (completion, priv->initial_model);
162       g_object_unref (G_OBJECT (reader));
163       return;
164     }
165 
166   gint i, count = json_reader_count_elements (reader);
167   for (i = 0; i < count; ++i)
168     {
169       if (!json_reader_read_element (reader, i))
170         continue;
171 
172       if (json_reader_is_object (reader))
173         {
174           const gchar * name = NULL;
175           const gchar * admin1 = NULL;
176           const gchar * country = NULL;
177           const gchar * longitude = NULL;
178           const gchar * latitude = NULL;
179           gboolean skip = FALSE;
180           if (json_reader_read_member (reader, "name"))
181             {
182               name = json_reader_get_string_value (reader);
183               json_reader_end_member (reader);
184             }
185           if (json_reader_read_member (reader, "admin1"))
186             {
187               admin1 = json_reader_get_string_value (reader);
188               json_reader_end_member (reader);
189             }
190           if (json_reader_read_member (reader, "country"))
191             {
192               country = json_reader_get_string_value (reader);
193               json_reader_end_member (reader);
194             }
195           if (json_reader_read_member (reader, "longitude"))
196             {
197               longitude = json_reader_get_string_value (reader);
198               json_reader_end_member (reader);
199             }
200           if (json_reader_read_member (reader, "latitude"))
201             {
202               latitude = json_reader_get_string_value (reader);
203               json_reader_end_member (reader);
204             }
205 
206       if (g_strcmp0(name, prev_name) == 0 &&
207           g_strcmp0(admin1, prev_admin1) == 0 &&
208           g_strcmp0(country, prev_country) == 0)
209         {
210           // Sometimes the data will have duplicate entries that only differ
211           // in longitude and latitude.  e.g. "rio de janeiro", "wellington"
212           skip = TRUE;
213         }
214 
215       if (!skip)
216         {
217           GtkTreeIter iter;
218           gtk_list_store_append (store, &iter);
219           gtk_list_store_set (store, &iter,
220                               CC_TIMEZONE_COMPLETION_ZONE, NULL,
221                               CC_TIMEZONE_COMPLETION_NAME, name,
222                               CC_TIMEZONE_COMPLETION_ADMIN1, admin1,
223                               CC_TIMEZONE_COMPLETION_COUNTRY, country,
224                               CC_TIMEZONE_COMPLETION_LONGITUDE, longitude,
225                               CC_TIMEZONE_COMPLETION_LATITUDE, latitude,
226                               -1);
227           gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE (store),
228                                            CC_TIMEZONE_COMPLETION_NAME,
229                                            sort_zone,
230                                            g_utf8_casefold(priv->request_text,
231                                              -1),
232                                            g_free);
233           gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (store),
234                                                 CC_TIMEZONE_COMPLETION_NAME,
235                                                 GTK_SORT_ASCENDING);
236         }
237 
238       prev_name = name;
239       prev_admin1 = admin1;
240       prev_country = country;
241     }
242 
243     json_reader_end_element (reader);
244   }
245 
246   if (strlen (priv->request_text) < 4)
247     {
248       gchar * lower_text = g_ascii_strdown (priv->request_text, -1);
249       if (g_strcmp0 (lower_text, "ut") == 0 ||
250           g_strcmp0 (lower_text, "utc") == 0)
251         {
252            GtkTreeIter iter;
253            gtk_list_store_append (store, &iter);
254            gtk_list_store_set (store, &iter,
255                                CC_TIMEZONE_COMPLETION_ZONE, "UTC",
256                                 CC_TIMEZONE_COMPLETION_NAME, "UTC",
257                                -1);
258         }
259       g_free (lower_text);
260     }
261 
262   save_and_use_model (completion, GTK_TREE_MODEL (store));
263   g_object_unref (G_OBJECT (reader));
264 }
265 
266 static void
geonames_data_ready(GObject * object,GAsyncResult * res,gpointer user_data)267 geonames_data_ready (GObject *object, GAsyncResult *res, gpointer user_data)
268 {
269   CcTimezoneCompletion * completion = CC_TIMEZONE_COMPLETION (user_data);
270   CcTimezoneCompletionPrivate * priv = completion->priv;
271   GError * error = NULL;
272   GInputStream * stream;
273   SoupMessage *message;
274 
275   stream = soup_request_send_finish (SOUP_REQUEST (object), res, &error);
276   if (stream == NULL)
277     {
278       if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
279         save_and_use_model (completion, priv->initial_model);
280       g_warning ("Could not connect to geoname lookup server: %s",
281           error->message);
282       g_error_free (error);
283       return;
284     }
285 
286   message = soup_request_http_get_message (SOUP_REQUEST_HTTP (object));
287   if (message->status_code == SOUP_STATUS_OK)
288     {
289       JsonParser *parser;
290 
291       parser = json_parser_new ();
292       json_parser_load_from_stream_async (parser, stream, priv->cancel, json_parse_ready, user_data);
293 
294       g_object_unref (parser);
295     }
296   else
297     {
298       g_warning ("Unable to fetch geonames (server responded with: %u %s)",
299                  message->status_code, message->reason_phrase);
300     }
301 
302   g_object_unref (message);
303   g_object_unref (stream);
304 }
305 
306 /* Returns message locale, with possible country info too like en_US */
307 static gchar *
get_locale(void)308 get_locale (void)
309 {
310   /* Check LANGUAGE, LC_ALL, LC_MESSAGES, and LANG, treat as colon-separated */
311   const gchar *env_names[] = {"LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG", NULL};
312   const gchar *env = NULL;
313   gint i;
314 
315   for (i = 0; env_names[i]; i++)
316     {
317       env = g_getenv (env_names[i]);
318       if (env != NULL && env[0] != 0)
319         break;
320     }
321 
322   if (env == NULL)
323     return NULL;
324 
325   /* Now, we split on colons as expected, but also on . and @ to filter out
326      extra pieces of locale we don't care about as we only use first chunk. */
327   gchar **split = g_strsplit_set (env, ":.@", 2);
328   if (split == NULL)
329     return NULL;
330 
331   if (split[0] == NULL)
332     {
333       g_strfreev (split);
334       return NULL;
335     }
336 
337   gchar *locale = g_strdup (split[0]);
338   g_strfreev (split);
339   return locale;
340 }
341 
342 static gchar *
get_version(void)343 get_version (void)
344 {
345   static gchar *version = NULL;
346 
347   if (version == NULL)
348     {
349       gchar *stdout = NULL;
350       g_spawn_command_line_sync ("lsb_release -rs", &stdout, NULL, NULL, NULL);
351 
352       if (stdout != NULL)
353         version = g_strstrip (stdout);
354       else
355         version = g_strdup("");
356     }
357 
358   return version;
359 }
360 
361 static gboolean
request_zones(CcTimezoneCompletion * completion)362 request_zones (CcTimezoneCompletion * completion)
363 {
364   CcTimezoneCompletionPrivate * priv = completion->priv;
365   SoupRequest *req;
366   GError *error = NULL;
367 
368   priv->queued_request = 0;
369 
370   if (priv->entry == NULL)
371     {
372       return FALSE;
373     }
374 
375   /* Cancel any ongoing request */
376   if (priv->cancel)
377     {
378       g_cancellable_cancel (priv->cancel);
379       g_object_unref (priv->cancel);
380       priv->cancel = g_cancellable_new ();
381     }
382   g_free (priv->request_text);
383 
384   const gchar * text = gtk_entry_get_text (priv->entry);
385   priv->request_text = g_strdup (text);
386 
387   gchar * escaped = g_uri_escape_string (text, NULL, FALSE);
388   gchar * version = get_version ();
389   gchar * locale = get_locale ();
390   gchar * url = g_strdup_printf (GEONAME_URL, escaped, version, locale);
391   g_free (locale);
392   g_free (escaped);
393 
394   req = soup_session_request (priv->soup_session, url, &error);
395   if (req)
396     {
397       soup_request_send_async (req, priv->cancel, geonames_data_ready, completion);
398       g_object_unref (req);
399     }
400   else
401     {
402       g_warning ("%s", error->message);
403       g_error_free (error);
404     }
405 
406   g_free (url);
407   return FALSE;
408 }
409 
410 static void
entry_changed(GtkEntry * entry,CcTimezoneCompletion * completion)411 entry_changed (GtkEntry * entry, CcTimezoneCompletion * completion)
412 {
413   CcTimezoneCompletionPrivate * priv = completion->priv;
414 
415   if (priv->queued_request)
416     {
417       g_source_remove (priv->queued_request);
418       priv->queued_request = 0;
419     }
420 
421   /* See if we've already got this one */
422   const gchar * text = gtk_entry_get_text (priv->entry);
423   gpointer data;
424   if (g_hash_table_lookup_extended (priv->request_table, text, NULL, &data))
425     {
426       gtk_entry_completion_set_model (GTK_ENTRY_COMPLETION (completion),
427           GTK_TREE_MODEL (data));
428     }
429   else
430     {
431       priv->queued_request = g_timeout_add (300, (GSourceFunc)request_zones,
432           completion);
433       gtk_entry_completion_set_model (GTK_ENTRY_COMPLETION (completion), NULL);
434     }
435   gtk_entry_completion_complete (GTK_ENTRY_COMPLETION (completion));
436 }
437 
438 static GtkWidget *
get_descendent(GtkWidget * parent,GType type)439 get_descendent (GtkWidget * parent, GType type)
440 {
441   if (g_type_is_a (G_OBJECT_TYPE (parent), type))
442     return parent;
443 
444   if (GTK_IS_CONTAINER (parent))
445     {
446       GList * children = gtk_container_get_children (GTK_CONTAINER (parent));
447       GList * iter;
448       for (iter = children; iter; iter = iter->next)
449         {
450           GtkWidget * found = get_descendent (GTK_WIDGET (iter->data), type);
451           if (found)
452             {
453               g_list_free (children);
454               return found;
455             }
456         }
457       g_list_free (children);
458     }
459 
460   return NULL;
461 }
462 
463 /*
464  * The popup window and its GtkTreeView are private to our parent completion
465  * object.  We can't get access to discover if there is a highlighted item or
466  * even if the window is showing right now.  So this is a super hack to find
467  * it by looking through our toplevel's window group and finding a window with
468  * a GtkTreeView that points at our model.  There should be only one ever, so
469  * we'll use the first one we find.
470  */
471 static GtkTreeView *
find_popup_treeview(GtkWidget * widget,GtkTreeModel * model)472 find_popup_treeview (GtkWidget * widget, GtkTreeModel * model)
473 {
474   GtkWidget * toplevel = gtk_widget_get_toplevel (widget);
475   if (!GTK_IS_WINDOW (toplevel))
476     return NULL;
477 
478   GtkWindowGroup * group = gtk_window_get_group (GTK_WINDOW (toplevel));
479   GList * windows = gtk_window_group_list_windows (group);
480   GList * iter;
481   for (iter = windows; iter; iter = iter->next)
482     {
483       if (iter->data == toplevel)
484         continue; // Skip our own window, we don't have it
485       GtkWidget * view = get_descendent (GTK_WIDGET (iter->data),
486           GTK_TYPE_TREE_VIEW);
487        if (view != NULL)
488          {
489           GtkTreeModel * tree_model =
490             gtk_tree_view_get_model (GTK_TREE_VIEW (view));
491           if (GTK_IS_TREE_MODEL_FILTER (tree_model))
492             tree_model = gtk_tree_model_filter_get_model (
493                 GTK_TREE_MODEL_FILTER (tree_model));
494           if (tree_model == model)
495             {
496               g_list_free (windows);
497               return GTK_TREE_VIEW (view);
498             }
499         }
500     }
501   g_list_free (windows);
502 
503   return NULL;
504 }
505 
506 static gboolean
entry_keypress(GtkEntry * entry,GdkEventKey * event,CcTimezoneCompletion * completion)507 entry_keypress (GtkEntry * entry, GdkEventKey  *event, CcTimezoneCompletion * completion)
508 {
509   if (event->keyval == GDK_KEY_ISO_Enter ||
510       event->keyval == GDK_KEY_KP_Enter ||
511       event->keyval == GDK_KEY_Return)
512     {
513       /* Make sure that user has a selection to choose, otherwise ignore */
514       GtkTreeModel * model = gtk_entry_completion_get_model (
515           GTK_ENTRY_COMPLETION (completion));
516       GtkTreeView * view = find_popup_treeview (GTK_WIDGET (entry), model);
517       if (view == NULL)
518        {
519          // Just beep if popup hasn't appeared yet.
520          gtk_widget_error_bell (GTK_WIDGET (entry));
521          return TRUE;
522        }
523 
524       GtkTreeSelection * sel = gtk_tree_view_get_selection (view);
525       GtkTreeModel * sel_model = NULL;
526       if (!gtk_tree_selection_get_selected (sel, &sel_model, NULL))
527         {
528           // No selection, we should help them out and select first item in list
529           GtkTreeIter iter;
530           if (gtk_tree_model_get_iter_first (sel_model, &iter))
531             gtk_tree_selection_select_iter (sel, &iter);
532           // And fall through to normal handler code
533         }
534     }
535 
536   return FALSE;
537 }
538 
539 void
cc_timezone_completion_watch_entry(CcTimezoneCompletion * completion,GtkEntry * entry)540 cc_timezone_completion_watch_entry (CcTimezoneCompletion * completion, GtkEntry * entry)
541 {
542   CcTimezoneCompletionPrivate * priv = completion->priv;
543 
544   if (priv->queued_request)
545     {
546       g_source_remove (priv->queued_request);
547       priv->queued_request = 0;
548     }
549   if (priv->entry)
550     {
551       g_signal_handler_disconnect (priv->entry, priv->changed_id);
552       priv->changed_id = 0;
553       g_signal_handler_disconnect (priv->entry, priv->keypress_id);
554       priv->keypress_id = 0;
555       g_object_remove_weak_pointer (G_OBJECT (priv->entry), (gpointer *)&priv->entry);
556       gtk_entry_set_completion (priv->entry, NULL);
557     }
558 
559   priv->entry = entry;
560 
561   if (entry)
562     {
563       guint id = g_signal_connect (entry, "changed",
564           G_CALLBACK (entry_changed), completion);
565       priv->changed_id = id;
566 
567       id = g_signal_connect (entry, "key-press-event",
568           G_CALLBACK (entry_keypress), completion);
569       priv->keypress_id = id;
570 
571       g_object_add_weak_pointer (G_OBJECT (entry), (gpointer *)&priv->entry);
572 
573       gtk_entry_set_completion (entry, GTK_ENTRY_COMPLETION (completion));
574     }
575 }
576 
577 static GtkListStore *
get_initial_model(void)578 get_initial_model (void)
579 {
580   TzDB * db = tz_load_db ();
581   GPtrArray * locations = tz_get_locations (db);
582 
583   GtkListStore * store = gtk_list_store_new (CC_TIMEZONE_COMPLETION_LAST,
584                                              G_TYPE_STRING,
585                                              G_TYPE_STRING,
586                                              G_TYPE_STRING,
587                                              G_TYPE_STRING,
588                                              G_TYPE_STRING,
589                                              G_TYPE_STRING);
590 
591   gint i;
592   for (i = 0; i < locations->len; ++i)
593     {
594       CcTimezoneLocation * loc = g_ptr_array_index (locations, i);
595       GtkTreeIter iter;
596       gtk_list_store_append (store, &iter);
597 
598       gchar * zone;
599       gchar * country;
600       gchar * en_name; // FIXME: need something better for non-English locales
601       gdouble longitude;
602       gdouble latitude;
603       g_object_get (loc, "zone", &zone, "country", &country, "en_name", &en_name,
604                     "longitude", &longitude, "latitude", &latitude,
605                     NULL);
606 
607       gchar * longitude_s = g_strdup_printf ("%f", longitude);
608       gchar * latitude_s=  g_strdup_printf ("%f", latitude);
609 
610       gtk_list_store_set (store, &iter,
611                           CC_TIMEZONE_COMPLETION_ZONE, NULL,
612                           CC_TIMEZONE_COMPLETION_NAME, en_name,
613                           CC_TIMEZONE_COMPLETION_COUNTRY, country,
614                           CC_TIMEZONE_COMPLETION_LONGITUDE, longitude_s,
615                           CC_TIMEZONE_COMPLETION_LATITUDE, latitude_s,
616                           -1);
617 
618       g_free (latitude_s);
619       g_free (longitude_s);
620       g_free (en_name);
621       g_free (country);
622       g_free (zone);
623     }
624 
625   GtkTreeIter iter;
626   gtk_list_store_append (store, &iter);
627   gtk_list_store_set (store, &iter,
628                       CC_TIMEZONE_COMPLETION_ZONE, "UTC",
629                       CC_TIMEZONE_COMPLETION_NAME, "UTC",
630                       -1);
631 
632   tz_db_free (db);
633   return store;
634 }
635 
636 static void
data_func(GtkCellLayout * cell_layout,GtkCellRenderer * cell,GtkTreeModel * tree_model,GtkTreeIter * iter,gpointer user_data)637 data_func (GtkCellLayout *cell_layout, GtkCellRenderer *cell,
638            GtkTreeModel *tree_model, GtkTreeIter *iter, gpointer user_data)
639 {
640   const gchar * name, * admin1, * country;
641 
642   gtk_tree_model_get (GTK_TREE_MODEL (tree_model), iter,
643                       CC_TIMEZONE_COMPLETION_NAME, &name,
644                       CC_TIMEZONE_COMPLETION_ADMIN1, &admin1,
645                       CC_TIMEZONE_COMPLETION_COUNTRY, &country,
646                       -1);
647 
648   gchar * user_name;
649   if (country == NULL || country[0] == 0)
650     {
651       user_name = g_strdup (name);
652     } else if (admin1 == NULL || admin1[0] == 0) {
653       user_name = g_strdup_printf ("%s <small>(%s)</small>", name, country);
654     } else {
655       user_name = g_strdup_printf ("%s <small>(%s, %s)</small>", name, admin1, country);
656     }
657 
658   g_object_set (G_OBJECT (cell), "markup", user_name, NULL);
659 }
660 
661 static void
cc_timezone_completion_class_init(CcTimezoneCompletionClass * klass)662 cc_timezone_completion_class_init (CcTimezoneCompletionClass *klass)
663 {
664   GObjectClass *object_class = G_OBJECT_CLASS (klass);
665 
666   g_type_class_add_private (klass, sizeof (CcTimezoneCompletionPrivate));
667 
668   object_class->dispose = cc_timezone_completion_dispose;
669   object_class->finalize = cc_timezone_completion_finalize;
670 
671   return;
672 }
673 
674 static void
cc_timezone_completion_init(CcTimezoneCompletion * self)675 cc_timezone_completion_init (CcTimezoneCompletion * self)
676 {
677   CcTimezoneCompletionPrivate *priv;
678 
679   self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
680                                             CC_TIMEZONE_COMPLETION_TYPE,
681                                             CcTimezoneCompletionPrivate);
682   priv = self->priv;
683 
684   priv->initial_model = GTK_TREE_MODEL (get_initial_model ());
685 
686   g_object_set (G_OBJECT (self),
687                 "text-column", CC_TIMEZONE_COMPLETION_NAME,
688                 "popup-set-width", FALSE,
689                 NULL);
690 
691   priv->cancel = g_cancellable_new ();
692 
693   priv->request_table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
694 
695   GtkCellRenderer * cell = gtk_cell_renderer_text_new ();
696   gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (self), cell, TRUE);
697   gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self), cell, data_func, NULL, NULL);
698 
699   priv->soup_session = soup_session_new ();
700 
701   return;
702 }
703 
704 static void
cc_timezone_completion_dispose(GObject * object)705 cc_timezone_completion_dispose (GObject * object)
706 {
707   G_OBJECT_CLASS (cc_timezone_completion_parent_class)->dispose (object);
708 
709   CcTimezoneCompletion * completion = CC_TIMEZONE_COMPLETION (object);
710   CcTimezoneCompletionPrivate * priv = completion->priv;
711 
712   if (priv->changed_id)
713     {
714       if (priv->entry)
715         g_signal_handler_disconnect (priv->entry, priv->changed_id);
716       priv->changed_id = 0;
717     }
718 
719   if (priv->keypress_id)
720     {
721       if (priv->entry)
722         g_signal_handler_disconnect (priv->entry, priv->keypress_id);
723       priv->keypress_id = 0;
724     }
725 
726   if (priv->entry != NULL)
727     {
728       gtk_entry_set_completion (priv->entry, NULL);
729       g_object_remove_weak_pointer (G_OBJECT (priv->entry), (gpointer *)&priv->entry);
730       priv->entry = NULL;
731     }
732 
733   if (priv->initial_model != NULL)
734     {
735       g_object_unref (G_OBJECT (priv->initial_model));
736       priv->initial_model = NULL;
737     }
738 
739   if (priv->queued_request)
740     {
741       g_source_remove (priv->queued_request);
742       priv->queued_request = 0;
743     }
744 
745   if (priv->cancel != NULL)
746     {
747       g_cancellable_cancel (priv->cancel);
748       g_object_unref (priv->cancel);
749       priv->cancel = NULL;
750     }
751 
752   if (priv->request_text != NULL)
753     {
754       g_free (priv->request_text);
755       priv->request_text = NULL;
756     }
757 
758   if (priv->request_table != NULL)
759     {
760       g_hash_table_destroy (priv->request_table);
761       priv->request_table = NULL;
762     }
763 
764   g_clear_object (&priv->soup_session);
765 
766   return;
767 }
768 
769 static void
cc_timezone_completion_finalize(GObject * object)770 cc_timezone_completion_finalize (GObject * object)
771 {
772   G_OBJECT_CLASS (cc_timezone_completion_parent_class)->finalize (object);
773   return;
774 }
775 
776 CcTimezoneCompletion *
cc_timezone_completion_new()777 cc_timezone_completion_new ()
778 {
779   CcTimezoneCompletion * self = g_object_new (CC_TIMEZONE_COMPLETION_TYPE, NULL);
780   return self;
781 }
782 
783