From 8e53d136220ab1967bf6295278845d81239ac250 Mon Sep 17 00:00:00 2001 From: Milan Crha Date: Thu, 11 Nov 2021 15:44:16 +0100 Subject: [PATCH] cal-config-weather: Bundle WeatherLocationEntry The libgweather 4 has the widget removed, thus prepare for it and bundle its copy from the libgweather 3 into the sources. --- CMakeLists.txt | 1 + po/POTFILES.in | 1 + src/modules/cal-config-weather/CMakeLists.txt | 5 + .../e-weather-location-entry.c | 885 ++++++++++++++++++ .../e-weather-location-entry.h | 49 + .../evolution-cal-config-weather.c | 4 +- 6 files changed, 944 insertions(+), 1 deletion(-) create mode 100644 src/modules/cal-config-weather/e-weather-location-entry.c create mode 100644 src/modules/cal-config-weather/e-weather-location-entry.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 02e9c86d74..45147c544d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -669,6 +669,7 @@ add_printable_option(ENABLE_WEATHER "Enable weather calendars" ON) if(ENABLE_WEATHER) pkg_check_modules_for_option(ENABLE_WEATHER "weather calendar" GWEATHER gweather-3.0>=${gweather_minimum_version}) + pkg_check_modules_for_option(ENABLE_WEATHER "weather calendar" GEO geocode-glib-1.0>=${geocode_glib_minimum_version}) endif(ENABLE_WEATHER) # maps in Contacts preview diff --git a/po/POTFILES.in b/po/POTFILES.in index 741155c552..af0c350485 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -417,6 +417,7 @@ src/modules/cal-config-google/e-cal-config-google.c src/modules/cal-config-google/e-cal-config-gtasks.c src/modules/cal-config-google/e-google-chooser-button.c src/modules/cal-config-local/evolution-cal-config-local.c +src/modules/cal-config-weather/e-weather-location-entry.c src/modules/cal-config-weather/evolution-cal-config-weather.c src/modules/cal-config-webcal/evolution-cal-config-webcal.c src/modules/cal-config-webdav-notes/evolution-cal-config-webdav-notes.c diff --git a/src/modules/cal-config-weather/CMakeLists.txt b/src/modules/cal-config-weather/CMakeLists.txt index b610b0b479..ef6db1720a 100644 --- a/src/modules/cal-config-weather/CMakeLists.txt +++ b/src/modules/cal-config-weather/CMakeLists.txt @@ -1,15 +1,20 @@ set(extra_deps) set(sources + e-weather-location-entry.h + e-weather-location-entry.c evolution-cal-config-weather.c ) set(extra_defines) set(extra_cflags + ${GEO_CFLAGS} ${GWEATHER_CFLAGS} ) set(extra_incdirs + ${GEO_INCLUDE_DIRS} ${GWEATHER_INCLUDE_DIRS} ) set(extra_ldflags + ${GEO_LDFLAGS} ${GWEATHER_LDFLAGS} ) diff --git a/src/modules/cal-config-weather/e-weather-location-entry.c b/src/modules/cal-config-weather/e-weather-location-entry.c new file mode 100644 index 0000000000..f4fb48c403 --- /dev/null +++ b/src/modules/cal-config-weather/e-weather-location-entry.c @@ -0,0 +1,885 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* Taken from libgweather 3, due to it being gone in the libgweather 4 + * + * SPDX-FileCopyrightText: (C) 2008 Red Hat, Inc. (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "evolution-config.h" + +#include +#include +#include + +#include + +#include "e-weather-location-entry.h" + +/* + * SECTION:gweatherlocationentry + * @Title: EWeatherLocationEntry + * + * A subclass of #GtkSearchEntry that provides autocompletion on + * #GWeatherLocations + */ + +struct _EWeatherLocationEntryPrivate { + GWeatherLocation *location; + GWeatherLocation *top; + gboolean show_named_timezones; + gboolean custom_text; + GCancellable *cancellable; + GtkTreeModel *model; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (EWeatherLocationEntry, e_weather_location_entry, GTK_TYPE_SEARCH_ENTRY) + +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 (EWeatherLocationEntry *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_E_WEATHER_LOCATION_ENTRY_COL_DISPLAY_NAME = 0, + LOC_E_WEATHER_LOCATION_ENTRY_COL_LOCATION, + LOC_E_WEATHER_LOCATION_ENTRY_COL_LOCAL_SORT_NAME, + LOC_E_WEATHER_LOCATION_ENTRY_COL_LOCAL_COMPARE_NAME, + LOC_E_WEATHER_LOCATION_ENTRY_COL_ENGLISH_COMPARE_NAME, + LOC_E_WEATHER_LOCATION_ENTRY_NUM_COLUMNS +}; + +enum PLACE +{ + PLACE_E_WEATHER_LOCATION_ENTRY_COL_DISPLAY_NAME = 0, + PLACE_E_WEATHER_LOCATION_ENTRY_COL_PLACE, + PLACE_E_WEATHER_LOCATION_ENTRY_COL_LOCAL_SORT_NAME, + PLACE_E_WEATHER_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 (EWeatherLocationEntry *entry); +static void _no_matches (GtkEntryCompletion *completion, EWeatherLocationEntry *entry); + +static void +e_weather_location_entry_init (EWeatherLocationEntry *entry) +{ + GtkEntryCompletion *completion; + EWeatherLocationEntryPrivate *priv; + + priv = entry->priv = e_weather_location_entry_get_instance_private (entry); + + completion = gtk_entry_completion_new (); + + gtk_entry_completion_set_popup_set_width (completion, FALSE); + gtk_entry_completion_set_text_column (completion, LOC_E_WEATHER_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), completion); + g_object_unref (completion); + + priv->custom_text = FALSE; + g_signal_connect (entry, "changed", + G_CALLBACK (entry_changed), NULL); +} + +static void +finalize (GObject *object) +{ + EWeatherLocationEntry *entry; + EWeatherLocationEntryPrivate *priv; + + entry = E_WEATHER_LOCATION_ENTRY (object); + priv = entry->priv; + + if (priv->location) + gweather_location_unref (priv->location); + if (priv->top) + gweather_location_unref (priv->top); + if (priv->model) + g_object_unref (priv->model); + + G_OBJECT_CLASS (e_weather_location_entry_parent_class)->finalize (object); +} + +static void +dispose (GObject *object) +{ + EWeatherLocationEntry *entry; + EWeatherLocationEntryPrivate *priv; + + entry = E_WEATHER_LOCATION_ENTRY (object); + priv = entry->priv; + + if (priv->cancellable) { + g_cancellable_cancel (priv->cancellable); + g_object_unref (priv->cancellable); + priv->cancellable = NULL; + } + + G_OBJECT_CLASS (e_weather_location_entry_parent_class)->dispose (object); +} + +static gint +tree_compare_local_name (GtkTreeModel *model, + GtkTreeIter *a, + GtkTreeIter *b, + gpointer user_data) +{ + gchar *name_a = NULL, *name_b = NULL; + gint res; + + gtk_tree_model_get (model, a, + LOC_E_WEATHER_LOCATION_ENTRY_COL_LOCAL_SORT_NAME, &name_a, + -1); + gtk_tree_model_get (model, b, + LOC_E_WEATHER_LOCATION_ENTRY_COL_LOCAL_SORT_NAME, &name_b, + -1); + + res = g_utf8_collate (name_a, name_b); + + g_free (name_a); + g_free (name_b); + + return res; +} + + +static void +constructed (GObject *object) +{ + EWeatherLocationEntry *entry; + GtkListStore *store = NULL; + GtkEntryCompletion *completion; + + entry = E_WEATHER_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)); + gtk_entry_completion_set_match_func (completion, matcher, NULL, NULL); + gtk_entry_completion_set_model (completion, GTK_TREE_MODEL (store)); + + G_OBJECT_CLASS (e_weather_location_entry_parent_class)->constructed (object); +} + +static void +e_weather_location_entry_class_init (EWeatherLocationEntryClass *location_entry_class) +{ + GObjectClass *object_class = G_OBJECT_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; + + g_object_class_install_property ( + object_class, PROP_TOP, + g_param_spec_boxed ("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_boxed ("location", + "Location", + "The selected GWeatherLocation", + GWEATHER_TYPE_LOCATION, + G_PARAM_READWRITE)); +} + +static void +set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + EWeatherLocationEntry *entry = E_WEATHER_LOCATION_ENTRY (object); + + switch (prop_id) { + case PROP_TOP: + entry->priv->top = g_value_dup_boxed (value); + break; + case PROP_SHOW_NAMED_TIMEZONES: + entry->priv->show_named_timezones = g_value_get_boolean (value); + break; + case PROP_LOCATION: + e_weather_location_entry_set_location (E_WEATHER_LOCATION_ENTRY (object), + g_value_get_boxed (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) +{ + EWeatherLocationEntry *entry = E_WEATHER_LOCATION_ENTRY (object); + + 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_boxed (value, entry->priv->location); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +entry_changed (EWeatherLocationEntry *entry) +{ + GtkEntryCompletion *completion; + const gchar *text; + + completion = gtk_entry_get_completion (GTK_ENTRY (entry)); + + if (entry->priv->cancellable) { + g_cancellable_cancel (entry->priv->cancellable); + g_object_unref (entry->priv->cancellable); + entry->priv->cancellable = NULL; + gtk_entry_completion_delete_action (completion, 0); + } + + gtk_entry_completion_set_match_func (gtk_entry_get_completion (GTK_ENTRY (entry)), matcher, NULL, NULL); + gtk_entry_completion_set_model (gtk_entry_get_completion (GTK_ENTRY (entry)), entry->priv->model); + + text = gtk_entry_get_text (GTK_ENTRY (entry)); + + if (text && *text) + entry->priv->custom_text = TRUE; + else + set_location_internal (entry, NULL, NULL, NULL); +} + +static void +set_location_internal (EWeatherLocationEntry *entry, + GtkTreeModel *model, + GtkTreeIter *iter, + GWeatherLocation *loc) +{ + EWeatherLocationEntryPrivate *priv; + gchar *name; + + priv = entry->priv; + + if (priv->location) + gweather_location_unref (priv->location); + + g_return_if_fail (iter == NULL || loc == NULL); + + if (iter) { + gtk_tree_model_get (model, iter, + LOC_E_WEATHER_LOCATION_ENTRY_COL_DISPLAY_NAME, &name, + LOC_E_WEATHER_LOCATION_ENTRY_COL_LOCATION, &priv->location, + -1); + gtk_entry_set_text (GTK_ENTRY (entry), name); + priv->custom_text = FALSE; + g_free (name); + } else if (loc) { + priv->location = gweather_location_ref (loc); + gtk_entry_set_text (GTK_ENTRY (entry), gweather_location_get_name (loc)); + priv->custom_text = FALSE; + } else { + priv->location = NULL; + gtk_entry_set_text (GTK_ENTRY (entry), ""); + priv->custom_text = TRUE; + } + + gtk_editable_set_position (GTK_EDITABLE (entry), -1); + g_object_notify (G_OBJECT (entry), "location"); +} + +/* + * e_weather_location_entry_set_location: + * @entry: a #EWeatherLocationEntry + * @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 +e_weather_location_entry_set_location (EWeatherLocationEntry *entry, + GWeatherLocation *loc) +{ + GtkEntryCompletion *completion; + GtkTreeModel *model; + GtkTreeIter iter; + GWeatherLocation *cmploc; + + g_return_if_fail (E_WEATHER_IS_LOCATION_ENTRY (entry)); + + completion = gtk_entry_get_completion (GTK_ENTRY (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_E_WEATHER_LOCATION_ENTRY_COL_LOCATION, &cmploc, + -1); + if (gweather_location_equal (loc, cmploc)) { + set_location_internal (entry, model, &iter, NULL); + gweather_location_unref (cmploc); + return; + } + + gweather_location_unref (cmploc); + } while (gtk_tree_model_iter_next (model, &iter)); + + set_location_internal (entry, model, NULL, loc); +} + +/* + * e_weather_location_entry_get_location: + * @entry: a #EWeatherLocationEntry + * + * Gets the location that was set by a previous call to + * e_weather_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 * +e_weather_location_entry_get_location (EWeatherLocationEntry *entry) +{ + g_return_val_if_fail (E_WEATHER_IS_LOCATION_ENTRY (entry), NULL); + + if (entry->priv->location) + return gweather_location_ref (entry->priv->location); + else + return NULL; +} + +/* + * e_weather_location_entry_has_custom_text: + * @entry: a #EWeatherLocationEntry + * + * Checks whether or not @entry's text has been modified by the user. + * Note that this does not mean that no location is associated with @entry. + * e_weather_location_entry_get_location() should be used for this. + * + * Return value: %TRUE if @entry's text was modified by the user, or %FALSE if + * it's set to the default text of a location. + **/ +gboolean +e_weather_location_entry_has_custom_text (EWeatherLocationEntry *entry) +{ + g_return_val_if_fail (E_WEATHER_IS_LOCATION_ENTRY (entry), FALSE); + + return entry->priv->custom_text; +} + +/* + * e_weather_location_entry_set_city: + * @entry: a #EWeatherLocationEntry + * @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 +e_weather_location_entry_set_city (EWeatherLocationEntry *entry, + const gchar *city_name, + const gchar *code) +{ + GtkEntryCompletion *completion; + GtkTreeModel *model; + GtkTreeIter iter; + GWeatherLocation *cmploc; + const gchar *cmpcode; + gchar *cmpname; + + g_return_val_if_fail (E_WEATHER_IS_LOCATION_ENTRY (entry), FALSE); + g_return_val_if_fail (code != NULL, FALSE); + + completion = gtk_entry_get_completion (GTK_ENTRY (entry)); + model = gtk_entry_completion_get_model (completion); + + gtk_tree_model_get_iter_first (model, &iter); + do { + gtk_tree_model_get (model, &iter, + LOC_E_WEATHER_LOCATION_ENTRY_COL_LOCATION, &cmploc, + -1); + + cmpcode = gweather_location_get_code (cmploc); + if (!cmpcode || strcmp (cmpcode, code) != 0) { + gweather_location_unref (cmploc); + continue; + } + + if (city_name) { + cmpname = gweather_location_get_city_name (cmploc); + if (!cmpname || strcmp (cmpname, city_name) != 0) { + gweather_location_unref (cmploc); + g_free (cmpname); + continue; + } + g_free (cmpname); + } + + set_location_internal (entry, model, &iter, NULL); + gweather_location_unref (cmploc); + 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 gchar *parent_display_name, + const gchar *parent_sort_local_name, + const gchar *parent_compare_local_name, + const gchar *parent_compare_english_name, + gboolean show_named_timezones) +{ + GWeatherLocation *child = NULL; + gchar *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_sort_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_E_WEATHER_LOCATION_ENTRY_COL_LOCATION, loc, + LOC_E_WEATHER_LOCATION_ENTRY_COL_DISPLAY_NAME, display_name, + LOC_E_WEATHER_LOCATION_ENTRY_COL_LOCAL_SORT_NAME, local_sort_name, + LOC_E_WEATHER_LOCATION_ENTRY_COL_LOCAL_COMPARE_NAME, local_compare_name, + LOC_E_WEATHER_LOCATION_ENTRY_COL_ENGLISH_COMPARE_NAME, english_compare_name, + -1); + + g_free (display_name); + g_free (local_sort_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_E_WEATHER_LOCATION_ENTRY_COL_LOCATION, loc, + LOC_E_WEATHER_LOCATION_ENTRY_COL_DISPLAY_NAME, gweather_location_get_name (loc), + LOC_E_WEATHER_LOCATION_ENTRY_COL_LOCAL_SORT_NAME, gweather_location_get_sort_name (loc), + LOC_E_WEATHER_LOCATION_ENTRY_COL_LOCAL_COMPARE_NAME, gweather_location_get_sort_name (loc), + LOC_E_WEATHER_LOCATION_ENTRY_COL_ENGLISH_COMPARE_NAME, gweather_location_get_english_sort_name (loc), + -1); + } + break; + + case GWEATHER_LOCATION_DETACHED: + g_warn_if_reached (); + break; + } + + g_clear_pointer (&child, gweather_location_unref); +} + +static gchar * +find_word (const gchar *full_name, + const gchar *word, + gint word_len, + gboolean whole_word, + gboolean is_first_word) +{ + gchar *p; + + if (word == NULL || *word == '\0') + return NULL; + + p = (gchar *) full_name - 1; + while ((p = strchr (p + 1, *word))) { + if (strncmp (p, word, word_len) != 0) + continue; + + if (p > (gchar *) full_name) { + gchar *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 == (gchar *) 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 gchar *key, + const gchar *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_warn_if_fail (len == strlen (key)); + return find_word (name, key, len, FALSE, is_first_word) != NULL; + } +} + +static gboolean +matcher (GtkEntryCompletion *completion, + const gchar *key, + GtkTreeIter *iter, + gpointer user_data) +{ + gchar *local_compare_name, *english_compare_name; + gboolean match; + + gtk_tree_model_get (gtk_entry_completion_get_model (completion), iter, + LOC_E_WEATHER_LOCATION_ENTRY_COL_LOCAL_COMPARE_NAME, &local_compare_name, + LOC_E_WEATHER_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) +{ + EWeatherLocationEntryPrivate *priv; + + priv = ((EWeatherLocationEntry *)entry)->priv; + + if (model != priv->model) { + GeocodePlace *place; + gchar *display_name; + GeocodeLocation *loc; + GWeatherLocation *location; + GWeatherLocation *scope = NULL; + const gchar *country_code; + + gtk_tree_model_get (model, iter, + PLACE_E_WEATHER_LOCATION_ENTRY_COL_PLACE, &place, + PLACE_E_WEATHER_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_find_nearest_city (scope, geocode_location_get_latitude (loc), geocode_location_get_longitude (loc)); + + 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_free (display_name); + } else { + set_location_internal (entry, model, iter, NULL); + } + + return TRUE; +} + +static gboolean +new_matcher (GtkEntryCompletion *completion, + const gchar *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 gchar *display_name; + gchar *normalized; + gchar *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); + + gtk_list_store_insert_with_values (user_data, NULL, -1, + PLACE_E_WEATHER_LOCATION_ENTRY_COL_PLACE, place, + PLACE_E_WEATHER_LOCATION_ENTRY_COL_DISPLAY_NAME, display_name, + PLACE_E_WEATHER_LOCATION_ENTRY_COL_LOCAL_SORT_NAME, compare_name, + PLACE_E_WEATHER_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) +{ + GList *places; + EWeatherLocationEntry *self = user_data; + GError *error = NULL; + GtkListStore *store = NULL; + GtkEntryCompletion *completion; + + places = geocode_forward_search_finish (GEOCODE_FORWARD (source_object), result, &error); + if (places == NULL) { + /* return without touching anything if cancelled (the entry might have been disposed) */ + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_clear_error (&error); + return; + } + + g_clear_error (&error); + completion = gtk_entry_get_completion (user_data); + gtk_entry_completion_set_match_func (completion, matcher, NULL, NULL); + gtk_entry_completion_set_model (completion, self->priv->model); + } else { + completion = gtk_entry_get_completion (user_data); + 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); + g_list_free (places); + gtk_entry_completion_set_match_func (completion, new_matcher, NULL, NULL); + gtk_entry_completion_set_model (completion, GTK_TREE_MODEL (store)); + g_object_unref (store); + } + + gtk_entry_completion_delete_action (completion, 0); + g_clear_object (&self->priv->cancellable); +} + +static void +_no_matches (GtkEntryCompletion *completion, + EWeatherLocationEntry *entry) +{ + const gchar *key = gtk_entry_get_text (GTK_ENTRY (entry)); + GeocodeForward *forward; + + if (entry->priv->cancellable) { + g_cancellable_cancel (entry->priv->cancellable); + g_object_unref (entry->priv->cancellable); + entry->priv->cancellable = NULL; + } else { + gtk_entry_completion_insert_action_text (completion, 0, _("Loading…")); + } + + entry->priv->cancellable = g_cancellable_new (); + + forward = geocode_forward_new_for_string(key); + geocode_forward_search_async (forward, entry->priv->cancellable, _got_places, entry); +} + +/* + * e_weather_location_entry_new: + * @top: the top-level location for the entry. + * + * Creates a new #EWeatherLocationEntry. + * + * @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 #EWeatherLocationEntry + **/ +GtkWidget * +e_weather_location_entry_new (GWeatherLocation *top) +{ + return g_object_new (E_WEATHER_TYPE_LOCATION_ENTRY, + "top", top, + NULL); +} diff --git a/src/modules/cal-config-weather/e-weather-location-entry.h b/src/modules/cal-config-weather/e-weather-location-entry.h new file mode 100644 index 0000000000..dd6a15875d --- /dev/null +++ b/src/modules/cal-config-weather/e-weather-location-entry.h @@ -0,0 +1,49 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* Taken from libgweather 3, due to it being gone in the libgweather 4 + * + * SPDX-FileCopyrightText: (C) 2008 Red Hat, Inc. (www.redhat.com) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#ifndef E_WEATHER_LOCATION_ENTRY_H +#define E_WEATHER_LOCATION_ENTRY_H 1 + +#include +#define GWEATHER_I_KNOW_THIS_IS_UNSTABLE +#include +#undef GWEATHER_I_KNOW_THIS_IS_UNSTABLE + +typedef struct _EWeatherLocationEntry EWeatherLocationEntry; +typedef struct _EWeatherLocationEntryClass EWeatherLocationEntryClass; +typedef struct _EWeatherLocationEntryPrivate EWeatherLocationEntryPrivate; + +#define E_WEATHER_TYPE_LOCATION_ENTRY (e_weather_location_entry_get_type ()) +#define E_WEATHER_LOCATION_ENTRY(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), E_WEATHER_TYPE_LOCATION_ENTRY, EWeatherLocationEntry)) +#define E_WEATHER_LOCATION_ENTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), E_WEATHER_TYPE_LOCATION_ENTRY, EWeatherLocationEntryClass)) +#define E_WEATHER_IS_LOCATION_ENTRY(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), E_WEATHER_TYPE_LOCATION_ENTRY)) +#define E_WEATHER_IS_LOCATION_ENTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), E_WEATHER_TYPE_LOCATION_ENTRY)) +#define E_WEATHER_LOCATION_ENTRY_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), E_WEATHER_TYPE_LOCATION_ENTRY, EWeatherLocationEntryClass)) + +struct _EWeatherLocationEntry { + GtkSearchEntry parent; + + /*< private >*/ + EWeatherLocationEntryPrivate *priv; +}; + +struct _EWeatherLocationEntryClass { + GtkSearchEntryClass parent_class; +}; + +GType e_weather_location_entry_get_type (void); +GtkWidget *e_weather_location_entry_new (GWeatherLocation *top); +void e_weather_location_entry_set_location (EWeatherLocationEntry *entry, + GWeatherLocation *loc); +GWeatherLocation *e_weather_location_entry_get_location (EWeatherLocationEntry *entry); +gboolean e_weather_location_entry_has_custom_text + (EWeatherLocationEntry *entry); +gboolean e_weather_location_entry_set_city (EWeatherLocationEntry *entry, + const gchar *city_name, + const gchar *code); + +#endif diff --git a/src/modules/cal-config-weather/evolution-cal-config-weather.c b/src/modules/cal-config-weather/evolution-cal-config-weather.c index 1b8a2cfcbd..423d6e2357 100644 --- a/src/modules/cal-config-weather/evolution-cal-config-weather.c +++ b/src/modules/cal-config-weather/evolution-cal-config-weather.c @@ -26,6 +26,8 @@ #include #undef GWEATHER_I_KNOW_THIS_IS_UNSTABLE +#include "e-weather-location-entry.h" + #include #if defined(HAVE_NL_LANGINFO) @@ -275,7 +277,7 @@ cal_config_weather_insert_widgets (ESourceConfigBackend *backend, world = gweather_location_get_world (); - widget = gweather_location_entry_new (world); + widget = e_weather_location_entry_new (world); e_source_config_insert_widget ( config, scratch_source, _("Location:"), widget); context->location_entry = g_object_ref (widget);