diff options
Diffstat (limited to 'src/timezone-completion.c')
-rw-r--r-- | src/timezone-completion.c | 214 |
1 files changed, 190 insertions, 24 deletions
diff --git a/src/timezone-completion.c b/src/timezone-completion.c index aaf6bdc..ad04d0c 100644 --- a/src/timezone-completion.c +++ b/src/timezone-completion.c @@ -24,6 +24,7 @@ with this program. If not, see <http://www.gnu.org/licenses/>. #include <json-glib/json-glib.h> #include <gdk/gdk.h> +#include <gdk/gdkkeysyms.h> #include <glib/gi18n.h> #include "timezone-completion.h" #include "tz.h" @@ -41,6 +42,7 @@ struct _TimezoneCompletionPrivate GtkEntry * entry; guint queued_request; guint changed_id; + guint keypress_id; GCancellable * cancel; gchar * request_text; GHashTable * request_table; @@ -79,12 +81,48 @@ save_and_use_model (TimezoneCompletion * completion, GtkTreeModel * model) gtk_entry_completion_set_match_func (GTK_ENTRY_COMPLETION (completion), match_func, NULL, NULL); gtk_entry_completion_set_model (GTK_ENTRY_COMPLETION (completion), model); - gtk_entry_completion_complete (GTK_ENTRY_COMPLETION (completion)); - /* By this time, the changed signal has come and gone. We didn't give a - model to use, so no popup appeared for user. Poke the entry again to show - popup in 300ms. */ - g_signal_emit_by_name (priv->entry, "changed"); + if (priv->entry != NULL) { + gtk_entry_completion_complete (GTK_ENTRY_COMPLETION (completion)); + + /* By this time, the changed signal has come and gone. We didn't give a + model to use, so no popup appeared for user. Poke the entry again to show + popup in 300ms. */ + g_signal_emit_by_name (priv->entry, "changed"); + } +} + +static gint +sort_zone (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, + gpointer user_data) +{ + /* Anything that has text as a prefix goes first, in mostly sorted order. + Then everything else goes after, in mostly sorted order. */ + const gchar *casefolded_text = (const gchar *)user_data; + + const gchar *namea = NULL, *nameb = NULL; + gtk_tree_model_get (model, a, TIMEZONE_COMPLETION_NAME, &namea, -1); + gtk_tree_model_get (model, b, TIMEZONE_COMPLETION_NAME, &nameb, -1); + + gchar *casefolded_namea = NULL, *casefolded_nameb = NULL; + casefolded_namea = g_utf8_casefold (namea, -1); + casefolded_nameb = g_utf8_casefold (nameb, -1); + + gboolean amatches = FALSE, bmatches = FALSE; + amatches = strncmp (casefolded_text, casefolded_namea, strlen(casefolded_text)) == 0; + bmatches = strncmp (casefolded_text, casefolded_nameb, strlen(casefolded_text)) == 0; + + gint rv; + if (amatches && !bmatches) + rv = -1; + else if (bmatches && !amatches) + rv = 1; + else + rv = g_utf8_collate (casefolded_namea, casefolded_nameb); + + g_free (casefolded_namea); + g_free (casefolded_nameb); + return rv; } static void @@ -99,14 +137,15 @@ json_parse_ready (GObject *object, GAsyncResult *res, gpointer user_data) json_parser_load_from_stream_finish (JSON_PARSER (object), res, &error); - if (priv->cancel && (error == NULL || error->code != G_IO_ERROR_CANCELLED)) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && priv->cancel) { g_cancellable_reset (priv->cancel); } if (error != NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + save_and_use_model (completion, priv->initial_model); g_warning ("Could not parse geoname JSON data: %s", error->message); g_error_free (error); - save_and_use_model (completion, priv->initial_model); return; } @@ -179,6 +218,13 @@ json_parse_ready (GObject *object, GAsyncResult *res, gpointer user_data) TIMEZONE_COMPLETION_LONGITUDE, longitude, TIMEZONE_COMPLETION_LATITUDE, latitude, -1); + gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE (store), + TIMEZONE_COMPLETION_NAME, sort_zone, + g_utf8_casefold(priv->request_text, -1), + g_free); + gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (store), + TIMEZONE_COMPLETION_NAME, + GTK_SORT_ASCENDING); } prev_name = name; @@ -189,6 +235,20 @@ json_parse_ready (GObject *object, GAsyncResult *res, gpointer user_data) json_reader_end_element (reader); } + if (strlen (priv->request_text) < 4) { + gchar * lower_text = g_ascii_strdown (priv->request_text, -1); + if (g_strcmp0 (lower_text, "ut") == 0 || + g_strcmp0 (lower_text, "utc") == 0) { + GtkTreeIter iter; + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, + TIMEZONE_COMPLETION_ZONE, "UTC", + TIMEZONE_COMPLETION_NAME, "UTC", + -1); + } + g_free (lower_text); + } + save_and_use_model (completion, GTK_TREE_MODEL (store)); g_object_unref (G_OBJECT (reader)); } @@ -203,14 +263,15 @@ geonames_data_ready (GObject *object, GAsyncResult *res, gpointer user_data) stream = g_file_read_finish (G_FILE (object), res, &error); - if (priv->cancel && (error == NULL || error->code != G_IO_ERROR_CANCELLED)) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && priv->cancel) { g_cancellable_reset (priv->cancel); } if (error != NULL) { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + save_and_use_model (completion, priv->initial_model); 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; } @@ -257,6 +318,7 @@ entry_changed (GtkEntry * entry, TimezoneCompletion * completion) if (priv->queued_request) { g_source_remove (priv->queued_request); + priv->queued_request = 0; } /* See if we've already got this one */ @@ -272,6 +334,94 @@ entry_changed (GtkEntry * entry, TimezoneCompletion * completion) gtk_entry_completion_complete (GTK_ENTRY_COMPLETION (completion)); } +static GtkWidget * +get_descendent (GtkWidget * parent, GType type) +{ + if (g_type_is_a (G_OBJECT_TYPE (parent), type)) + return parent; + + if (GTK_IS_CONTAINER (parent)) { + GList * children = gtk_container_get_children (GTK_CONTAINER (parent)); + GList * iter; + for (iter = children; iter; iter = iter->next) { + GtkWidget * found = get_descendent (GTK_WIDGET (iter->data), type); + if (found) { + g_list_free (children); + return found; + } + } + g_list_free (children); + } + + return NULL; +} + +/** + * The popup window and its GtkTreeView are private to our parent completion + * object. We can't get access to discover if there is a highlighted item or + * even if the window is showing right now. So this is a super hack to find + * it by looking through our toplevel's window group and finding a window with + * a GtkTreeView that points at our model. There should be only one ever, so + * we'll use the first one we find. + */ +static GtkTreeView * +find_popup_treeview (GtkWidget * widget, GtkTreeModel * model) +{ + GtkWidget * toplevel = gtk_widget_get_toplevel (widget); + if (!GTK_IS_WINDOW (toplevel)) + return NULL; + + GtkWindowGroup * group = gtk_window_get_group (GTK_WINDOW (toplevel)); + GList * windows = gtk_window_group_list_windows (group); + GList * iter; + for (iter = windows; iter; iter = iter->next) { + if (iter->data == toplevel) + continue; // Skip our own window, we don't have it + GtkWidget * view = get_descendent (GTK_WIDGET (iter->data), GTK_TYPE_TREE_VIEW); + if (view != NULL) { + GtkTreeModel * tree_model = gtk_tree_view_get_model (GTK_TREE_VIEW (view)); + if (GTK_IS_TREE_MODEL_FILTER (tree_model)) + tree_model = gtk_tree_model_filter_get_model (GTK_TREE_MODEL_FILTER (tree_model)); + if (tree_model == model) { + g_list_free (windows); + return GTK_TREE_VIEW (view); + } + } + } + g_list_free (windows); + + return NULL; +} + +static gboolean +entry_keypress (GtkEntry * entry, GdkEventKey *event, TimezoneCompletion * completion) +{ + if (event->keyval == GDK_ISO_Enter || + event->keyval == GDK_KP_Enter || + event->keyval == GDK_Return) { + /* Make sure that user has a selection to choose, otherwise ignore */ + GtkTreeModel * model = gtk_entry_completion_get_model (GTK_ENTRY_COMPLETION (completion)); + GtkTreeView * view = find_popup_treeview (GTK_WIDGET (entry), model); + if (view == NULL) { + // Just beep if popup hasn't appeared yet. + gtk_widget_error_bell (GTK_WIDGET (entry)); + return TRUE; + } + + GtkTreeSelection * sel = gtk_tree_view_get_selection (view); + GtkTreeModel * sel_model = NULL; + if (!gtk_tree_selection_get_selected (sel, &sel_model, NULL)) { + // No selection, we should help them out and select first item in list + GtkTreeIter iter; + if (gtk_tree_model_get_iter_first (sel_model, &iter)) + gtk_tree_selection_select_iter (sel, &iter); + // And fall through to normal handler code + } + } + + return FALSE; +} + void timezone_completion_watch_entry (TimezoneCompletion * completion, GtkEntry * entry) { @@ -282,15 +432,22 @@ timezone_completion_watch_entry (TimezoneCompletion * completion, GtkEntry * ent priv->queued_request = 0; } if (priv->entry) { - g_source_remove (priv->changed_id); + g_signal_handler_disconnect (priv->entry, priv->changed_id); + g_signal_handler_disconnect (priv->entry, priv->keypress_id); g_object_remove_weak_pointer (G_OBJECT (priv->entry), (gpointer *)&priv->entry); + gtk_entry_set_completion (priv->entry, NULL); } guint id = g_signal_connect (entry, "changed", G_CALLBACK (entry_changed), completion); priv->changed_id = id; + id = g_signal_connect (entry, "key-press-event", G_CALLBACK (entry_keypress), completion); + priv->keypress_id = id; + priv->entry = entry; g_object_add_weak_pointer (G_OBJECT (entry), (gpointer *)&priv->entry); + + gtk_entry_set_completion (entry, GTK_ENTRY_COMPLETION (completion)); } static GtkListStore * @@ -332,6 +489,13 @@ get_initial_model (void) g_free (name); } + GtkTreeIter iter; + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, + TIMEZONE_COMPLETION_ZONE, "UTC", + TIMEZONE_COMPLETION_NAME, "UTC", + -1); + tz_db_free (db); return store; } @@ -340,29 +504,24 @@ 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); + gtk_tree_model_get (GTK_TREE_MODEL (tree_model), iter, + TIMEZONE_COMPLETION_NAME, &name, + TIMEZONE_COMPLETION_ADMIN1, &admin1, + TIMEZONE_COMPLETION_COUNTRY, &country, + -1); gchar * user_name; - if (admin1 == NULL || admin1[0] == 0) { + if (country == NULL || country[0] == 0) { + user_name = g_strdup (name); + } else 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 void @@ -410,10 +569,17 @@ timezone_completion_dispose (GObject * object) TimezoneCompletionPrivate * priv = TIMEZONE_COMPLETION_GET_PRIVATE (completion); if (priv->changed_id) { - g_source_remove (priv->changed_id); + if (priv->entry) + g_signal_handler_disconnect (priv->entry, priv->changed_id); priv->changed_id = 0; } + if (priv->keypress_id) { + if (priv->entry) + g_signal_handler_disconnect (priv->entry, priv->keypress_id); + priv->keypress_id = 0; + } + if (priv->entry != NULL) { g_object_remove_weak_pointer (G_OBJECT (priv->entry), (gpointer *)&priv->entry); } |