/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ /* * Copyright (C) 2012 Red Hat * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * * Written by: * Jasper St. Pierre */ /* Timezone page {{{1 */ #define PAGE_ID "timezone" #include "config.h" #include "gis-timezone-page.h" #include #include #include #include #define GNOME_DESKTOP_USE_UNSTABLE_API #include #include #include #include #include #include #include "timedated.h" #include "cc-datetime-resources.h" #include "timezone-resources.h" #include "cc-timezone-map.h" #include "gis-bubble-widget.h" #include "gis-page-header.h" #include "gis-location-entry.h" #define DEFAULT_TZ "Europe/London" #define DESKTOP_ID "gnome-datetime-panel" #define CLOCK_SCHEMA "org.gnome.desktop.interface" #define CLOCK_FORMAT_KEY "clock-format" /* FIXME: Drop this when we depend on a version of GeoClue which has * https://gitlab.freedesktop.org/geoclue/geoclue/-/merge_requests/73 */ typedef GClueSimple MyGClueSimple; G_DEFINE_AUTOPTR_CLEANUP_FUNC (MyGClueSimple, g_object_unref) static void stop_geolocation (GisTimezonePage *page); struct _GisTimezonePagePrivate { GtkWidget *map; GtkWidget *search_entry; GtkWidget *search_overlay; GCancellable *geoclue_cancellable; GClueClient *geoclue_client; GClueSimple *geoclue_simple; gboolean in_geoclue_callback; GWeatherLocation *current_location; Timedate1 *dtm; GCancellable *dtm_cancellable; GnomeWallClock *clock; GDesktopClockFormat clock_format; gboolean in_search; gulong search_entry_text_changed_id; }; typedef struct _GisTimezonePagePrivate GisTimezonePagePrivate; G_DEFINE_TYPE_WITH_PRIVATE (GisTimezonePage, gis_timezone_page, GIS_TYPE_PAGE); static void set_timezone_cb (GObject *source, GAsyncResult *res, gpointer user_data) { Timedate1 *dtm = TIMEDATE1 (source); g_autoptr(GError) error = NULL; if (!timedate1_call_set_timezone_finish (dtm, res, &error)) { /* TODO: display any error in a user friendly way */ g_warning ("Could not set system timezone: %s", error->message); } } static void queue_set_timezone (GisTimezonePage *page, const char *tzid) { GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page); /* for now just do it */ timedate1_call_set_timezone (priv->dtm, tzid, TRUE, priv->dtm_cancellable, set_timezone_cb, page); } static void set_location (GisTimezonePage *page, GWeatherLocation *location) { GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page); g_clear_object (&priv->current_location); gtk_widget_set_visible (priv->search_overlay, (location == NULL)); gis_page_set_complete (GIS_PAGE (page), (location != NULL)); if (location) { GTimeZone *zone; const char *tzid; priv->current_location = g_object_ref (location); zone = gweather_location_get_timezone (location); tzid = g_time_zone_get_identifier (zone); cc_timezone_map_set_timezone (CC_TIMEZONE_MAP (priv->map), tzid); /* If this location is manually set, stop waiting for geolocation. */ if (!priv->in_geoclue_callback) stop_geolocation (page); } } static void on_location_notify (GClueSimple *simple, GParamSpec *pspec, gpointer user_data) { GisTimezonePage *page = user_data; GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page); GClueLocation *location; gdouble latitude, longitude; g_autoptr(GWeatherLocation) world = gweather_location_get_world (); g_autoptr(GWeatherLocation) glocation = NULL; location = gclue_simple_get_location (simple); latitude = gclue_location_get_latitude (location); longitude = gclue_location_get_longitude (location); glocation = gweather_location_find_nearest_city (world, latitude, longitude); priv->in_geoclue_callback = TRUE; set_location (page, glocation); priv->in_geoclue_callback = FALSE; } static void on_geoclue_simple_ready (GObject *source_object, GAsyncResult *res, gpointer user_data) { GisTimezonePage *page = user_data; GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page); g_autoptr(GError) local_error = NULL; g_autoptr(MyGClueSimple) geoclue_simple = NULL; /* This function may be called in an idle callback once @page has been * disposed, if going through cancellation. So don’t dereference @priv or * @page until the error has been checked. */ geoclue_simple = gclue_simple_new_finish (res, &local_error); if (local_error != NULL) { if (!g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_info ("Failed to connect to GeoClue2 service: %s", local_error->message); return; } priv->geoclue_simple = g_steal_pointer (&geoclue_simple); priv->geoclue_client = gclue_simple_get_client (priv->geoclue_simple); gclue_client_set_distance_threshold (priv->geoclue_client, GEOCODE_LOCATION_ACCURACY_CITY); g_signal_connect (priv->geoclue_simple, "notify::location", G_CALLBACK (on_location_notify), page); on_location_notify (priv->geoclue_simple, NULL, page); } static void get_location_from_geoclue_async (GisTimezonePage *page) { GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page); gclue_simple_new (DESKTOP_ID, GCLUE_ACCURACY_LEVEL_CITY, priv->geoclue_cancellable, on_geoclue_simple_ready, page); } static void entry_text_changed (GtkEditable *editable, gpointer user_data) { GisTimezonePage *page = GIS_TIMEZONE_PAGE (user_data); GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page); stop_geolocation (GIS_TIMEZONE_PAGE (user_data)); g_signal_handler_disconnect (priv->search_entry, priv->search_entry_text_changed_id); priv->search_entry_text_changed_id = 0; } static void entry_location_changed (GObject *object, GParamSpec *param, GisTimezonePage *page) { GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page); GisLocationEntry *entry = GIS_LOCATION_ENTRY (object); g_autoptr(GWeatherLocation) location = NULL; location = gis_location_entry_get_location (entry); if (!location) return; priv->in_search = TRUE; set_location (page, location); priv->in_search = FALSE; } #define GETTEXT_PACKAGE_TIMEZONES "gnome-control-center-2.0-timezones" static char * translated_city_name (TzLocation *loc) { char *country; char *name; char *zone_translated; char **split_translated; gint length; /* Load the translation for it */ zone_translated = g_strdup (dgettext (GETTEXT_PACKAGE_TIMEZONES, loc->zone)); g_strdelimit (zone_translated, "_", ' '); split_translated = g_regex_split_simple ("[\\x{2044}\\x{2215}\\x{29f8}\\x{ff0f}/]", zone_translated, 0, 0); g_free (zone_translated); length = g_strv_length (split_translated); country = gnome_get_country_from_code (loc->country, NULL); /* Translators: "city, country" */ name = g_strdup_printf (C_("timezone loc", "%s, %s"), split_translated[length-1], country); g_free (country); g_strfreev (split_translated); return name; } static void update_timezone (GisTimezonePage *page, TzLocation *location) { GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page); char *tz_desc; char *bubble_text; char *city_country; char *utc_label; char *time_label; GTimeZone *zone; GDateTime *date; gboolean use_ampm; if (priv->clock_format == G_DESKTOP_CLOCK_FORMAT_12H) use_ampm = TRUE; else use_ampm = FALSE; zone = g_time_zone_new (location->zone); date = g_date_time_new_now (zone); g_time_zone_unref (zone); /* Update the text bubble in the timezone map */ city_country = translated_city_name (location); /* Translators: UTC here means the Coordinated Universal Time. * %:::z will be replaced by the offset from UTC e.g. UTC+02 */ utc_label = g_date_time_format (date, _("UTC%:::z")); if (use_ampm) /* Translators: This is the time format used in 12-hour mode. */ time_label = g_date_time_format (date, _("%l:%M %p")); else /* Translators: This is the time format used in 24-hour mode. */ time_label = g_date_time_format (date, _("%R")); /* Translators: "timezone (utc shift)" */ tz_desc = g_strdup_printf (C_("timezone map", "%s (%s)"), g_date_time_get_timezone_abbreviation (date), utc_label); bubble_text = g_strdup_printf ("%s\n" "%s\n" "%s", tz_desc, city_country, time_label); cc_timezone_map_set_bubble_text (CC_TIMEZONE_MAP (priv->map), bubble_text); g_free (tz_desc); g_free (city_country); g_free (utc_label); g_free (time_label); g_free (bubble_text); g_date_time_unref (date); } static void map_location_changed (CcTimezoneMap *map, TzLocation *location, GisTimezonePage *page) { GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page); gtk_widget_set_visible (priv->search_overlay, (location == NULL)); gis_page_set_complete (GIS_PAGE (page), (location != NULL)); if (!priv->in_search) gtk_editable_set_text (GTK_EDITABLE (priv->search_entry), ""); update_timezone (page, location); queue_set_timezone (page, location->zone); } static void on_clock_changed (GnomeWallClock *clock, GParamSpec *pspec, GisTimezonePage *page) { GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page); TzLocation *location; if (!gtk_widget_get_mapped (priv->map)) return; if (gtk_widget_is_visible (priv->search_overlay)) return; location = cc_timezone_map_get_location (CC_TIMEZONE_MAP (priv->map)); if (location) update_timezone (page, location); } static void entry_mapped (GtkWidget *widget, gpointer user_data) { gtk_widget_grab_focus (widget); } static void stop_geolocation (GisTimezonePage *page) { GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page); if (priv->geoclue_cancellable) { g_cancellable_cancel (priv->geoclue_cancellable); g_clear_object (&priv->geoclue_cancellable); } if (priv->geoclue_client) { gclue_client_call_stop (priv->geoclue_client, NULL, NULL, NULL); priv->geoclue_client = NULL; } g_clear_object (&priv->geoclue_simple); } static void gis_timezone_page_root (GtkWidget *widget) { GisTimezonePage *page = GIS_TIMEZONE_PAGE (widget); GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page); GTK_WIDGET_CLASS (gis_timezone_page_parent_class)->root (widget); if (priv->geoclue_cancellable == NULL) { priv->geoclue_cancellable = g_cancellable_new (); get_location_from_geoclue_async (page); } } static void gis_timezone_page_constructed (GObject *object) { GisTimezonePage *page = GIS_TIMEZONE_PAGE (object); GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page); GError *error; GSettings *settings; G_OBJECT_CLASS (gis_timezone_page_parent_class)->constructed (object); priv->dtm_cancellable = g_cancellable_new (); error = NULL; priv->dtm = timedate1_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, "org.freedesktop.timedate1", "/org/freedesktop/timedate1", priv->dtm_cancellable, &error); if (priv->dtm == NULL) { g_error ("Failed to create proxy for timedated: %s", error->message); exit (1); } priv->clock = g_object_new (GNOME_TYPE_WALL_CLOCK, NULL); g_signal_connect (priv->clock, "notify::clock", G_CALLBACK (on_clock_changed), page); settings = g_settings_new (CLOCK_SCHEMA); priv->clock_format = g_settings_get_enum (settings, CLOCK_FORMAT_KEY); g_object_unref (settings); set_location (page, NULL); priv->search_entry_text_changed_id = g_signal_connect (priv->search_entry, "changed", G_CALLBACK (entry_text_changed), page); g_signal_connect (priv->search_entry, "notify::location", G_CALLBACK (entry_location_changed), page); g_signal_connect (priv->search_entry, "map", G_CALLBACK (entry_mapped), page); g_signal_connect (priv->map, "location-changed", G_CALLBACK (map_location_changed), page); gtk_widget_show (GTK_WIDGET (page)); } static void gis_timezone_page_dispose (GObject *object) { GisTimezonePage *page = GIS_TIMEZONE_PAGE (object); GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page); stop_geolocation (page); if (priv->dtm_cancellable != NULL) { g_cancellable_cancel (priv->dtm_cancellable); g_clear_object (&priv->dtm_cancellable); } g_clear_object (&priv->dtm); g_clear_object (&priv->clock); G_OBJECT_CLASS (gis_timezone_page_parent_class)->dispose (object); } static void gis_timezone_page_locale_changed (GisPage *page) { gis_page_set_title (GIS_PAGE (page), _("Time Zone")); } static gboolean gis_timezone_page_apply (GisPage *page, GCancellable *cancellable) { /* Once the user accepts the location, it would be unkind to change it if * GeoClue suddenly tells us we're somewhere else. */ stop_geolocation (GIS_TIMEZONE_PAGE (page)); return FALSE; } static void gis_timezone_page_class_init (GisTimezonePageClass *klass) { GisPageClass *page_class = GIS_PAGE_CLASS (klass); GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/initial-setup/gis-timezone-page.ui"); gtk_widget_class_bind_template_child_private (widget_class, GisTimezonePage, map); gtk_widget_class_bind_template_child_private (widget_class, GisTimezonePage, search_entry); gtk_widget_class_bind_template_child_private (widget_class, GisTimezonePage, search_overlay); page_class->page_id = PAGE_ID; page_class->locale_changed = gis_timezone_page_locale_changed; page_class->apply = gis_timezone_page_apply; object_class->constructed = gis_timezone_page_constructed; object_class->dispose = gis_timezone_page_dispose; widget_class->root = gis_timezone_page_root; } static void gis_timezone_page_init (GisTimezonePage *page) { g_resources_register (timezone_get_resource ()); g_resources_register (datetime_get_resource ()); g_type_ensure (CC_TYPE_TIMEZONE_MAP); g_type_ensure (GIS_TYPE_BUBBLE_WIDGET); g_type_ensure (GIS_TYPE_PAGE_HEADER); g_type_ensure (GIS_TYPE_LOCATION_ENTRY); gtk_widget_init_template (GTK_WIDGET (page)); } GisPage * gis_prepare_timezone_page (GisDriver *driver) { return g_object_new (GIS_TYPE_TIMEZONE_PAGE, "driver", driver, NULL); }