From e05fb7b3e36c052baf0dd607ddeb22c0a2b5cbde Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:47:04 +0200 Subject: Adding upstream version 43.2. Signed-off-by: Daniel Baumann --- .../pages/timezone/gis-location-entry.c | 871 +++++++++++++++++++++ 1 file changed, 871 insertions(+) create mode 100644 gnome-initial-setup/pages/timezone/gis-location-entry.c (limited to 'gnome-initial-setup/pages/timezone/gis-location-entry.c') diff --git a/gnome-initial-setup/pages/timezone/gis-location-entry.c b/gnome-initial-setup/pages/timezone/gis-location-entry.c new file mode 100644 index 0000000..de8fdb8 --- /dev/null +++ b/gnome-initial-setup/pages/timezone/gis-location-entry.c @@ -0,0 +1,871 @@ +/* gweather-location-entry.c - Location-selecting text entry + * + * SPDX-FileCopyrightText: 2008, Red Hat, Inc. + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "config.h" + +#include "gis-location-entry.h" + +#include +#include +#include +#include + +/** + * GisLocationEntry: + * + * A subclass of [class@Gtk.SearchEntry] that provides autocompletion on + * [struct@GWeather.Location]s. + * + */ + +struct _GisLocationEntryPrivate { + GtkWidget *entry; + GWeatherLocation *location; + GWeatherLocation *top; + gboolean show_named_timezones; + GCancellable *cancellable; + GtkTreeModel *model; +}; + +static void editable_iface_init (GtkEditableInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GisLocationEntry, gis_location_entry, GTK_TYPE_WIDGET, + G_ADD_PRIVATE (GisLocationEntry) + G_IMPLEMENT_INTERFACE (GTK_TYPE_EDITABLE, editable_iface_init)); + +enum { + PROP_0, + + PROP_TOP, + PROP_SHOW_NAMED_TIMEZONES, + PROP_LOCATION, + + LAST_PROP +}; + +static void set_property (GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec); +static void get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec); + +static void set_location_internal (GisLocationEntry *entry, + GtkTreeModel *model, + GtkTreeIter *iter, + GWeatherLocation *loc); +static void +fill_location_entry_model (GtkListStore *store, GWeatherLocation *loc, + const char *parent_display_name, + const char *parent_sort_local_name, + const char *parent_compare_local_name, + const char *parent_compare_english_name, + gboolean show_named_timezones); + +enum LOC +{ + LOC_GIS_LOCATION_ENTRY_COL_DISPLAY_NAME = 0, + LOC_GIS_LOCATION_ENTRY_COL_LOCATION, + LOC_GIS_LOCATION_ENTRY_COL_LOCAL_SORT_NAME, + LOC_GIS_LOCATION_ENTRY_COL_LOCAL_COMPARE_NAME, + LOC_GIS_LOCATION_ENTRY_COL_ENGLISH_COMPARE_NAME, + LOC_GIS_LOCATION_ENTRY_NUM_COLUMNS +}; + +enum PLACE +{ + PLACE_GIS_LOCATION_ENTRY_COL_DISPLAY_NAME = 0, + PLACE_GIS_LOCATION_ENTRY_COL_PLACE, + PLACE_GIS_LOCATION_ENTRY_COL_LOCAL_SORT_NAME, + PLACE_GIS_LOCATION_ENTRY_COL_LOCAL_COMPARE_NAME +}; + +static gboolean matcher (GtkEntryCompletion *completion, const char *key, + GtkTreeIter *iter, gpointer user_data); +static gboolean match_selected (GtkEntryCompletion *completion, + GtkTreeModel *model, + GtkTreeIter *iter, + gpointer entry); +static void entry_changed (GisLocationEntry *entry); +static void _no_matches (GtkEntryCompletion *completion, GisLocationEntry *entry); + +static GtkEditable* +gis_location_entry_get_delegate (GtkEditable *editable) +{ + GisLocationEntry *entry = GIS_LOCATION_ENTRY (editable); + GisLocationEntryPrivate *priv = gis_location_entry_get_instance_private (entry); + + return GTK_EDITABLE (priv->entry); +} + +static void +editable_iface_init (GtkEditableInterface *iface) +{ + iface->get_delegate = gis_location_entry_get_delegate; +} + +static void +gis_location_entry_init (GisLocationEntry *entry) +{ + GtkEntryCompletion *completion; + GisLocationEntryPrivate *priv; + + priv = entry->priv = gis_location_entry_get_instance_private (entry); + + priv->entry = gtk_entry_new (); + gtk_widget_set_parent (priv->entry, GTK_WIDGET (entry)); + gtk_editable_init_delegate (GTK_EDITABLE (entry)); + + completion = gtk_entry_completion_new (); + + gtk_entry_completion_set_popup_set_width (completion, TRUE); + gtk_entry_completion_set_text_column (completion, LOC_GIS_LOCATION_ENTRY_COL_DISPLAY_NAME); + gtk_entry_completion_set_match_func (completion, matcher, NULL, NULL); + gtk_entry_completion_set_inline_completion (completion, TRUE); + + g_signal_connect (completion, "match-selected", + G_CALLBACK (match_selected), entry); + + g_signal_connect (completion, "no-matches", + G_CALLBACK (_no_matches), entry); + + gtk_entry_set_completion (GTK_ENTRY (entry->priv->entry), completion); + g_object_unref (completion); + + g_signal_connect (entry, "changed", + G_CALLBACK (entry_changed), NULL); +} + +static void +finalize (GObject *object) +{ + GisLocationEntry *entry; + GisLocationEntryPrivate *priv; + + entry = GIS_LOCATION_ENTRY (object); + priv = entry->priv; + + g_clear_object (&priv->location); + g_clear_object (&priv->top); + g_clear_object (&priv->model); + + G_OBJECT_CLASS (gis_location_entry_parent_class)->finalize (object); +} + +static void +dispose (GObject *object) +{ + GisLocationEntry *entry; + GisLocationEntryPrivate *priv; + + entry = GIS_LOCATION_ENTRY (object); + priv = entry->priv; + + if (priv->cancellable) { + g_cancellable_cancel (priv->cancellable); + g_object_unref (priv->cancellable); + priv->cancellable = NULL; + } + + gtk_editable_finish_delegate (GTK_EDITABLE (entry)); + g_clear_pointer (&priv->entry, gtk_widget_unparent); + + G_OBJECT_CLASS (gis_location_entry_parent_class)->dispose (object); +} + +static int +tree_compare_local_name (GtkTreeModel *model, + GtkTreeIter *a, + GtkTreeIter *b, + gpointer user_data) +{ + g_autofree gchar *name_a = NULL, *name_b = NULL; + + gtk_tree_model_get (model, a, + LOC_GIS_LOCATION_ENTRY_COL_LOCAL_SORT_NAME, &name_a, + -1); + gtk_tree_model_get (model, b, + LOC_GIS_LOCATION_ENTRY_COL_LOCAL_SORT_NAME, &name_b, + -1); + + return g_utf8_collate (name_a, name_b); +} + + +static void +constructed (GObject *object) +{ + GisLocationEntry *entry; + GtkListStore *store = NULL; + GtkEntryCompletion *completion; + + entry = GIS_LOCATION_ENTRY (object); + + if (!entry->priv->top) + entry->priv->top = gweather_location_get_world (); + + store = gtk_list_store_new (5, G_TYPE_STRING, GWEATHER_TYPE_LOCATION, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING); + gtk_tree_sortable_set_default_sort_func (GTK_TREE_SORTABLE (store), + tree_compare_local_name, NULL, NULL); + fill_location_entry_model (store, entry->priv->top, NULL, NULL, NULL, NULL, entry->priv->show_named_timezones); + + entry->priv->model = GTK_TREE_MODEL (store); + completion = gtk_entry_get_completion (GTK_ENTRY (entry->priv->entry)); + gtk_entry_completion_set_match_func (completion, matcher, NULL, NULL); + gtk_entry_completion_set_model (completion, GTK_TREE_MODEL (store)); + + G_OBJECT_CLASS (gis_location_entry_parent_class)->constructed (object); +} + +static void +gis_location_entry_class_init (GisLocationEntryClass *location_entry_class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (location_entry_class); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (location_entry_class); + + object_class->constructed = constructed; + object_class->finalize = finalize; + object_class->set_property = set_property; + object_class->get_property = get_property; + object_class->dispose = dispose; + + /* properties */ + g_object_class_install_property ( + object_class, PROP_TOP, + g_param_spec_object ("top", + "Top Location", + "The GWeatherLocation whose children will be used to fill in the entry", + GWEATHER_TYPE_LOCATION, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY)); + g_object_class_install_property ( + object_class, PROP_SHOW_NAMED_TIMEZONES, + g_param_spec_boolean ("show-named-timezones", + "Show named timezones", + "Whether UTC and other named timezones are shown in the list of locations", + FALSE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); + g_object_class_install_property ( + object_class, PROP_LOCATION, + g_param_spec_object ("location", + "Location", + "The selected GWeatherLocation", + GWEATHER_TYPE_LOCATION, + G_PARAM_READWRITE)); + + gtk_editable_install_properties (object_class, LAST_PROP); + + gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT); +} + +static void +set_property (GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + GisLocationEntry *entry = GIS_LOCATION_ENTRY (object); + + if (gtk_editable_delegate_set_property (object, prop_id, value, pspec)) + return; + + switch (prop_id) { + case PROP_TOP: + entry->priv->top = g_value_dup_object (value); + break; + case PROP_SHOW_NAMED_TIMEZONES: + entry->priv->show_named_timezones = g_value_get_boolean (value); + break; + case PROP_LOCATION: + gis_location_entry_set_location (GIS_LOCATION_ENTRY (object), + g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + GisLocationEntry *entry = GIS_LOCATION_ENTRY (object); + + if (gtk_editable_delegate_get_property (object, prop_id, value, pspec)) + return; + + switch (prop_id) { + case PROP_SHOW_NAMED_TIMEZONES: + g_value_set_boolean (value, entry->priv->show_named_timezones); + break; + case PROP_LOCATION: + g_value_set_object (value, entry->priv->location); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +entry_changed (GisLocationEntry *entry) +{ + GtkEntryCompletion *completion; + const gchar *text; + + completion = gtk_entry_get_completion (GTK_ENTRY (entry->priv->entry)); + + if (entry->priv->cancellable) { + g_cancellable_cancel (entry->priv->cancellable); + g_object_unref (entry->priv->cancellable); + entry->priv->cancellable = NULL; + } + + gtk_entry_completion_set_match_func (completion, matcher, NULL, NULL); + gtk_entry_completion_set_model (completion, entry->priv->model); + + text = gtk_editable_get_text (GTK_EDITABLE (entry)); + + if (!text || *text == '\0') + set_location_internal (entry, NULL, NULL, NULL); +} + +static void +set_entry_text (GisLocationEntry *entry, + const char *text) +{ + GisLocationEntryPrivate *priv = entry->priv; + + if (g_strcmp0 (gtk_editable_get_text (GTK_EDITABLE (priv->entry)), text) != 0) + gtk_editable_set_text (GTK_EDITABLE (priv->entry), text); +} + +static void +set_location_internal (GisLocationEntry *entry, + GtkTreeModel *model, + GtkTreeIter *iter, + GWeatherLocation *loc) +{ + GisLocationEntryPrivate *priv; + char *name; + + priv = entry->priv; + + g_clear_object (&priv->location); + + g_assert (iter == NULL || loc == NULL); + + g_signal_handlers_block_by_func (entry, entry_changed, NULL); + + if (iter) { + gtk_tree_model_get (model, iter, + LOC_GIS_LOCATION_ENTRY_COL_DISPLAY_NAME, &name, + LOC_GIS_LOCATION_ENTRY_COL_LOCATION, &priv->location, + -1); + set_entry_text (entry, name); + g_free (name); + } else if (loc) { + priv->location = g_object_ref (loc); + set_entry_text (entry, gweather_location_get_name (loc)); + } else { + priv->location = NULL; + set_entry_text (entry, ""); + } + + g_signal_handlers_unblock_by_func (entry, entry_changed, NULL); + + gtk_editable_set_position (GTK_EDITABLE (entry), -1); + g_object_notify (G_OBJECT (entry), "location"); +} + +/** + * gis_location_entry_set_location: + * @entry: a #GisLocationEntry + * @loc: (allow-none): a #GWeatherLocation in @entry, or %NULL to + * clear @entry + * + * Sets @entry's location to @loc, and updates the text of the + * entry accordingly. + * Note that if the database contains a location that compares + * equal to @loc, that will be chosen in place of @loc. + **/ +void +gis_location_entry_set_location (GisLocationEntry *entry, + GWeatherLocation *loc) +{ + GtkEntryCompletion *completion; + GtkTreeModel *model; + GtkTreeIter iter; + g_autoptr(GWeatherLocation) cmploc = NULL; + + g_return_if_fail (GIS_IS_LOCATION_ENTRY (entry)); + + completion = gtk_entry_get_completion (GTK_ENTRY (entry->priv->entry)); + model = gtk_entry_completion_get_model (completion); + + if (loc == NULL) { + set_location_internal (entry, model, NULL, NULL); + return; + } + + gtk_tree_model_get_iter_first (model, &iter); + do { + gtk_tree_model_get (model, &iter, + LOC_GIS_LOCATION_ENTRY_COL_LOCATION, &cmploc, + -1); + if (gweather_location_equal (loc, cmploc)) { + set_location_internal (entry, model, &iter, NULL); + return; + } + + g_clear_object (&cmploc); + } while (gtk_tree_model_iter_next (model, &iter)); + + set_location_internal (entry, model, NULL, loc); +} + +/** + * gis_location_entry_get_location: + * @entry: a #GisLocationEntry + * + * Gets the location that was set by a previous call to + * gis_location_entry_set_location() or was selected by the user. + * + * Return value: (transfer full) (allow-none): the selected location + * (which you must unref when you are done with it), or %NULL if no + * location is selected. + **/ +GWeatherLocation * +gis_location_entry_get_location (GisLocationEntry *entry) +{ + g_return_val_if_fail (GIS_IS_LOCATION_ENTRY (entry), NULL); + + if (entry->priv->location) + return g_object_ref (entry->priv->location); + else + return NULL; +} + +/** + * gis_location_entry_set_city: + * @entry: a #GisLocationEntry + * @city_name: (allow-none): the city name, or %NULL + * @code: the METAR station code + * + * Sets @entry's location to a city with the given @code, and given + * @city_name, if non-%NULL. If there is no matching city, sets + * @entry's location to %NULL. + * + * Return value: %TRUE if @entry's location could be set to a matching city, + * %FALSE otherwise. + **/ +gboolean +gis_location_entry_set_city (GisLocationEntry *entry, + const char *city_name, + const char *code) +{ + GtkEntryCompletion *completion; + GtkTreeModel *model; + GtkTreeIter iter; + const char *cmpcode; + + g_return_val_if_fail (GIS_IS_LOCATION_ENTRY (entry), FALSE); + g_return_val_if_fail (code != NULL, FALSE); + + completion = gtk_entry_get_completion (GTK_ENTRY (entry->priv->entry)); + model = gtk_entry_completion_get_model (completion); + + gtk_tree_model_get_iter_first (model, &iter); + do { + g_autoptr(GWeatherLocation) cmploc = NULL; + gtk_tree_model_get (model, &iter, + LOC_GIS_LOCATION_ENTRY_COL_LOCATION, &cmploc, + -1); + + cmpcode = gweather_location_get_code (cmploc); + if (!cmpcode || strcmp (cmpcode, code) != 0) { + continue; + } + + if (city_name) { + g_autofree gchar *cmpname = gweather_location_get_city_name (cmploc); + if (!cmpname || strcmp (cmpname, city_name) != 0) { + continue; + } + } + + set_location_internal (entry, model, &iter, NULL); + return TRUE; + } while (gtk_tree_model_iter_next (model, &iter)); + + set_location_internal (entry, model, NULL, NULL); + + return FALSE; +} + +static void +fill_location_entry_model (GtkListStore *store, GWeatherLocation *loc, + const char *parent_display_name, + const char *parent_sort_local_name, + const char *parent_compare_local_name, + const char *parent_compare_english_name, + gboolean show_named_timezones) +{ + g_autoptr(GWeatherLocation) child = NULL; + char *display_name, *local_sort_name, *local_compare_name, *english_compare_name; + + switch (gweather_location_get_level (loc)) { + case GWEATHER_LOCATION_WORLD: + case GWEATHER_LOCATION_REGION: + /* Ignore these levels of hierarchy; just recurse, passing on + * the names from the parent node. + */ + while ((child = gweather_location_next_child (loc, child))) + fill_location_entry_model (store, child, + parent_display_name, + parent_sort_local_name, + parent_compare_local_name, + parent_compare_english_name, + show_named_timezones); + break; + + case GWEATHER_LOCATION_COUNTRY: + /* Recurse, initializing the names to the country name */ + while ((child = gweather_location_next_child (loc, child))) + fill_location_entry_model (store, child, + gweather_location_get_name (loc), + gweather_location_get_sort_name (loc), + gweather_location_get_sort_name (loc), + gweather_location_get_english_sort_name (loc), + show_named_timezones); + break; + + case GWEATHER_LOCATION_ADM1: + /* Recurse, adding the ADM1 name to the country name */ + /* Translators: this is the name of a location followed by a region, for example: + * 'London, United Kingdom' + * You shouldn't need to translate this string unless the language has a different comma. + */ + display_name = g_strdup_printf (_("%s, %s"), gweather_location_get_name (loc), parent_display_name); + local_sort_name = g_strdup_printf ("%s, %s", parent_sort_local_name, gweather_location_get_sort_name (loc)); + local_compare_name = g_strdup_printf ("%s, %s", gweather_location_get_sort_name (loc), parent_compare_local_name); + english_compare_name = g_strdup_printf ("%s, %s", gweather_location_get_english_sort_name (loc), parent_compare_english_name); + + while ((child = gweather_location_next_child (loc, child))) + fill_location_entry_model (store, child, + display_name, local_sort_name, local_compare_name, english_compare_name, + show_named_timezones); + + g_free (display_name); + g_free (local_compare_name); + g_free (english_compare_name); + break; + + case GWEATHER_LOCATION_CITY: + /* If there are multiple () children, we use the one + * closest to the city center. + * + * Locations are already sorted by increasing distance from + * the city. + */ + case GWEATHER_LOCATION_WEATHER_STATION: + /* with no parent */ + /* Translators: this is the name of a location followed by a region, for example: + * 'London, United Kingdom' + * You shouldn't need to translate this string unless the language has a different comma. + */ + display_name = g_strdup_printf (_("%s, %s"), + gweather_location_get_name (loc), parent_display_name); + local_sort_name = g_strdup_printf ("%s, %s", + parent_sort_local_name, gweather_location_get_sort_name (loc)); + local_compare_name = g_strdup_printf ("%s, %s", + gweather_location_get_sort_name (loc), parent_compare_local_name); + english_compare_name = g_strdup_printf ("%s, %s", + gweather_location_get_english_sort_name (loc), parent_compare_english_name); + + gtk_list_store_insert_with_values (store, NULL, -1, + LOC_GIS_LOCATION_ENTRY_COL_LOCATION, loc, + LOC_GIS_LOCATION_ENTRY_COL_DISPLAY_NAME, display_name, + LOC_GIS_LOCATION_ENTRY_COL_LOCAL_SORT_NAME, local_sort_name, + LOC_GIS_LOCATION_ENTRY_COL_LOCAL_COMPARE_NAME, local_compare_name, + LOC_GIS_LOCATION_ENTRY_COL_ENGLISH_COMPARE_NAME, english_compare_name, + -1); + + g_free (display_name); + g_free (local_compare_name); + g_free (english_compare_name); + break; + + case GWEATHER_LOCATION_NAMED_TIMEZONE: + if (show_named_timezones) { + gtk_list_store_insert_with_values (store, NULL, -1, + LOC_GIS_LOCATION_ENTRY_COL_LOCATION, loc, + LOC_GIS_LOCATION_ENTRY_COL_DISPLAY_NAME, gweather_location_get_name (loc), + LOC_GIS_LOCATION_ENTRY_COL_LOCAL_SORT_NAME, gweather_location_get_sort_name (loc), + LOC_GIS_LOCATION_ENTRY_COL_LOCAL_COMPARE_NAME, gweather_location_get_sort_name (loc), + LOC_GIS_LOCATION_ENTRY_COL_ENGLISH_COMPARE_NAME, gweather_location_get_english_sort_name (loc), + -1); + } + break; + + case GWEATHER_LOCATION_DETACHED: + g_assert_not_reached (); + } +} + +static char * +find_word (const char *full_name, const char *word, int word_len, + gboolean whole_word, gboolean is_first_word) +{ + char *p; + + if (word == NULL || *word == '\0') + return NULL; + + p = (char *)full_name - 1; + while ((p = strchr (p + 1, *word))) { + if (strncmp (p, word, word_len) != 0) + continue; + + if (p > (char *)full_name) { + char *prev = g_utf8_prev_char (p); + + /* Make sure p points to the start of a word */ + if (g_unichar_isalpha (g_utf8_get_char (prev))) + continue; + + /* If we're matching the first word of the key, it has to + * match the first word of the location, city, state, or + * country, or the abbreviation (in parenthesis). + * Eg, it either matches the start of the string + * (which we already know it doesn't at this point) or + * it is preceded by the string ", " or "(" (which isn't actually + * a perfect test. FIXME) + */ + if (is_first_word) { + if (prev == (char *)full_name || + ((prev - 1 <= full_name && strncmp (prev - 1, ", ", 2) != 0) + && *prev != '(')) + continue; + } + } + + if (whole_word && g_unichar_isalpha (g_utf8_get_char (p + word_len))) + continue; + + return p; + } + return NULL; +} + +static gboolean +match_compare_name (const char *key, const char *name) +{ + gboolean is_first_word = TRUE; + size_t len; + + /* Ignore whitespace before the string */ + key += strspn (key, " "); + + /* All but the last word in KEY must match a full word from NAME, + * in order (but possibly skipping some words from NAME). + */ + len = strcspn (key, " "); + while (key[len]) { + name = find_word (name, key, len, TRUE, is_first_word); + if (!name) + return FALSE; + + key += len; + while (*key && !g_unichar_isalpha (g_utf8_get_char (key))) + key = g_utf8_next_char (key); + while (*name && !g_unichar_isalpha (g_utf8_get_char (name))) + name = g_utf8_next_char (name); + + len = strcspn (key, " "); + is_first_word = FALSE; + } + + /* The last word in KEY must match a prefix of a following word in NAME */ + if (len == 0) { + return TRUE; + } else { + // if we get here, key[len] == 0, so... + g_assert (len == strlen(key)); + return find_word (name, key, len, FALSE, is_first_word) != NULL; + } +} + +static gboolean +matcher (GtkEntryCompletion *completion, const char *key, + GtkTreeIter *iter, gpointer user_data) +{ + char *local_compare_name, *english_compare_name; + gboolean match; + + gtk_tree_model_get (gtk_entry_completion_get_model (completion), iter, + LOC_GIS_LOCATION_ENTRY_COL_LOCAL_COMPARE_NAME, &local_compare_name, + LOC_GIS_LOCATION_ENTRY_COL_ENGLISH_COMPARE_NAME, &english_compare_name, + -1); + + match = match_compare_name (key, local_compare_name) || + match_compare_name (key, english_compare_name) || + g_ascii_strcasecmp (key, english_compare_name) == 0; + + g_free (local_compare_name); + g_free (english_compare_name); + return match; +} + +static gboolean +match_selected (GtkEntryCompletion *completion, + GtkTreeModel *model, + GtkTreeIter *iter, + gpointer entry) +{ + GisLocationEntryPrivate *priv; + + priv = ((GisLocationEntry *)entry)->priv; + + if (model != priv->model) { + GeocodePlace *place; + char *display_name; + GeocodeLocation *loc; + GWeatherLocation *location; + GWeatherLocation *scope = NULL; + const char* country_code; + + gtk_tree_model_get (model, iter, + PLACE_GIS_LOCATION_ENTRY_COL_PLACE, &place, + PLACE_GIS_LOCATION_ENTRY_COL_DISPLAY_NAME, &display_name, + -1); + + country_code = geocode_place_get_country_code (place); + if (country_code != NULL && gweather_location_get_level (priv->top) == GWEATHER_LOCATION_WORLD) + scope = gweather_location_find_by_country_code (priv->top, country_code); + if (!scope) + scope = priv->top; + + loc = geocode_place_get_location (place); + location = gweather_location_new_detached (display_name, + NULL, + geocode_location_get_latitude (loc), + geocode_location_get_longitude (loc)); + + set_location_internal (entry, model, NULL, location); + + g_object_unref (place); + g_object_unref (location); + g_free (display_name); + } else { + set_location_internal (entry, model, iter, NULL); + } + return TRUE; +} + +static gboolean +new_matcher (GtkEntryCompletion *completion, const char *key, + GtkTreeIter *iter, gpointer user_data) +{ + return TRUE; +} + +static void +fill_store (gpointer data, gpointer user_data) +{ + GeocodePlace *place = GEOCODE_PLACE (data); + GeocodeLocation *loc = geocode_place_get_location (place); + const char *display_name; + char *normalized; + char *compare_name; + + display_name = geocode_location_get_description (loc); + normalized = g_utf8_normalize (display_name, -1, G_NORMALIZE_ALL); + compare_name = g_utf8_casefold (normalized, -1); + + g_debug ("Adding geocode match %s", display_name); + + gtk_list_store_insert_with_values (user_data, NULL, -1, + PLACE_GIS_LOCATION_ENTRY_COL_PLACE, place, + PLACE_GIS_LOCATION_ENTRY_COL_DISPLAY_NAME, display_name, + PLACE_GIS_LOCATION_ENTRY_COL_LOCAL_SORT_NAME, compare_name, + PLACE_GIS_LOCATION_ENTRY_COL_LOCAL_COMPARE_NAME, compare_name, + -1); + + g_free (normalized); + g_free (compare_name); +} + +static void +_got_places (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autolist(GeocodePlace) places = NULL; + GisLocationEntry *self = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GtkListStore) store = NULL; + GtkEntryCompletion *completion; + + places = geocode_forward_search_finish (GEOCODE_FORWARD (source_object), result, &error); + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + /* return without touching anything if cancelled (the entry might have been disposed) */ + g_debug ("Geocode query cancelled"); + return; + } + + self = GIS_LOCATION_ENTRY (user_data); + completion = gtk_entry_get_completion (GTK_ENTRY (self->priv->entry)); + + if (places == NULL) { + g_debug ("No geocode results, restoring default model"); + gtk_entry_completion_set_match_func (completion, matcher, NULL, NULL); + gtk_entry_completion_set_model (completion, self->priv->model); + } else { + store = gtk_list_store_new (5, G_TYPE_STRING, GEOCODE_TYPE_PLACE, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING); + gtk_tree_sortable_set_default_sort_func (GTK_TREE_SORTABLE (store), + tree_compare_local_name, NULL, NULL); + g_list_foreach (places, fill_store, store); + gtk_entry_completion_set_match_func (completion, new_matcher, NULL, NULL); + gtk_entry_completion_set_model (completion, GTK_TREE_MODEL (store)); + } + + g_clear_object (&self->priv->cancellable); +} + +static void +_no_matches (GtkEntryCompletion *completion, GisLocationEntry *entry) { + const gchar *key = gtk_editable_get_text (GTK_EDITABLE (entry->priv->entry)); + GeocodeForward *forward; + + if (entry->priv->cancellable) { + g_cancellable_cancel (entry->priv->cancellable); + g_object_unref (entry->priv->cancellable); + entry->priv->cancellable = NULL; + } + + entry->priv->cancellable = g_cancellable_new (); + + g_debug ("Starting geocode query for %s", key); + forward = geocode_forward_new_for_string(key); + geocode_forward_search_async (forward, entry->priv->cancellable, _got_places, entry); +} + +/** + * gis_location_entry_new: + * @top: the top-level location for the entry. + * + * Creates a new #GisLocationEntry. + * + * @top will normally be the location returned from + * gweather_location_get_world(), but you can create an entry that + * only accepts a smaller set of locations if you want. + * + * Return value: the new #GisLocationEntry + **/ +GtkWidget * +gis_location_entry_new (GWeatherLocation *top) +{ + return g_object_new (GIS_TYPE_LOCATION_ENTRY, + "top", top, + NULL); +} -- cgit v1.2.3