diff options
Diffstat (limited to 'src/service.c')
-rw-r--r-- | src/service.c | 1357 |
1 files changed, 1289 insertions, 68 deletions
diff --git a/src/service.c b/src/service.c index f5e16a0..7287336 100644 --- a/src/service.c +++ b/src/service.c @@ -3,6 +3,7 @@ * * Authors: * Charles Kerr <charles.kerr@canonical.com> + * Ted Gould <ted@canonical.com> * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License version 3, as published @@ -17,25 +18,33 @@ * with this program. If not, see <http://www.gnu.org/licenses/>. */ -#include <locale.h> +#include "config.h" + +#include <string.h> /* strstr() */ #include <glib/gi18n.h> #include <gio/gio.h> +#include "planner-eds.h" +#include "timezone-file.h" +#include "timezone-geoclue.h" #include "service.h" +#include "settings-shared.h" +#include "utils.h" -/* FIXME: remove -test */ -#define BUS_NAME "com.canonical.indicator.datetime-test" +#define BUS_NAME "datetime.indicator" #define BUS_PATH "/com/canonical/indicator/datetime" +#define SKEW_CHECK_INTERVAL_SEC 10 +#define SKEW_DIFF_THRESHOLD_USEC ((SKEW_CHECK_INTERVAL_SEC+5) * G_USEC_PER_SEC) + G_DEFINE_TYPE (IndicatorDatetimeService, indicator_datetime_service, G_TYPE_OBJECT) -/* signals enum */ enum { - NAME_LOST, + SIGNAL_NAME_LOST, LAST_SIGNAL }; @@ -69,7 +78,7 @@ enum static const char * const menu_names[N_PROFILES] = { "desktop", - "desktop_greeter" + "greeter" }; struct ProfileMenuInfo @@ -85,15 +94,38 @@ struct ProfileMenuInfo struct _IndicatorDatetimeServicePrivate { + GCancellable * cancellable; + + GSettings * settings; + + IndicatorDatetimeTimezone * tz_file; + IndicatorDatetimeTimezone * tz_geoclue; + IndicatorDatetimePlanner * planner; + guint own_id; - GSimpleActionGroup * actions; guint actions_export_id; - struct ProfileMenuInfo menus[N_PROFILES]; + GDBusConnection * conn; + guint rebuild_id; int rebuild_flags; - GDBusConnection * conn; - GCancellable * cancellable; + struct ProfileMenuInfo menus[N_PROFILES]; + + GDateTime * skew_time; + guint skew_timer; + + guint header_timer; + guint timezone_timer; + + /* Which year/month to show in the calendar, + and which day should get the cursor. + This value is reflected in the calendar action's state */ + time_t calendar_date; + + GSimpleActionGroup * actions; GSimpleAction * header_action; + GSimpleAction * calendar_action; + + GDBusProxy * login1_manager; gboolean replace; }; @@ -104,6 +136,20 @@ typedef IndicatorDatetimeServicePrivate priv_t; **** ***/ +static void +indicator_clear_timer (guint * tag) +{ + if (*tag) + { + g_source_remove (*tag); + *tag = 0; + } +} + +/*** +**** +***/ + static void rebuild_now (IndicatorDatetimeService * self, int section); static void rebuild_soon (IndicatorDatetimeService * self, int section); @@ -112,21 +158,25 @@ rebuild_header_soon (IndicatorDatetimeService * self) { rebuild_soon (self, SECTION_HEADER); } + static inline void -rebuild_calendar_soon (IndicatorDatetimeService * self) +rebuild_calendar_section_soon (IndicatorDatetimeService * self) { rebuild_soon (self, SECTION_CALENDAR); } + static inline void rebuild_appointments_section_soon (IndicatorDatetimeService * self) { rebuild_soon (self, SECTION_APPOINTMENTS); } + static inline void rebuild_locations_section_soon (IndicatorDatetimeService * self) { rebuild_soon (self, SECTION_LOCATIONS); } + static inline void rebuild_settings_section_soon (IndicatorDatetimeService * self) { @@ -134,68 +184,976 @@ rebuild_settings_section_soon (IndicatorDatetimeService * self) } /*** -**** +**** TIMEZONE TIMER ***/ +/* + * Periodically rebuild the sections that have time format strings + * that are dependent on the current time: + * + * 1. appointment menuitems' time format strings depend on the + * current time; for example, they don't show the day of week + * if the appointment is today. + * + * 2. location menuitems' time format strings depend on the + * current time; for example, they don't show the day of the week + * if the local date and location date are the same. + * + * 3. the "local date" menuitem in the calendar section is, + * obviously, dependent on the local time. + * + * In short, we want to update whenever the number of days between two zone + * might have changed. We do that by updating when the day changes in either zone. + * + * Since not all UTC offsets are evenly divisible by hours + * (examples: Newfoundland UTC-03:30, Nepal UTC+05:45), refreshing on the hour + * is not enough. We need to refresh at HH:00, HH:15, HH:30, and HH:45. + */ + +static guint +calculate_seconds_until_next_fifteen_minutes (void) +{ + char * str; + gint minute; + guint seconds; + GTimeSpan diff; + GDateTime * now; + GDateTime * next; + GDateTime * start_of_next; + + now = g_date_time_new_now_local (); + + minute = g_date_time_get_minute (now); + minute = 15 - (minute % 15); + next = g_date_time_add_minutes (now, minute); + start_of_next = g_date_time_new_local (g_date_time_get_year (next), + g_date_time_get_month (next), + g_date_time_get_day_of_month (next), + g_date_time_get_hour (next), + g_date_time_get_minute (next), + 1); + + str = g_date_time_format (start_of_next, "%F %T"); + g_debug ("%s %s the next timestamp rebuild will be at %s", G_STRLOC, G_STRFUNC, str); + g_free (str); + + diff = g_date_time_difference (start_of_next, now); + seconds = (diff + (G_TIME_SPAN_SECOND-1)) / G_TIME_SPAN_SECOND; + + g_date_time_unref (start_of_next); + g_date_time_unref (next); + g_date_time_unref (now); + + return seconds; +} + +static void start_timezone_timer (IndicatorDatetimeService * self); + +static gboolean +on_timezone_timer (gpointer gself) +{ + IndicatorDatetimeService * self = INDICATOR_DATETIME_SERVICE (gself); + + rebuild_soon (self, SECTION_CALENDAR | + SECTION_APPOINTMENTS | + SECTION_LOCATIONS); + + /* Restarting the timer to recalculate the interval. This helps us to hit + our marks despite clock skew, suspend+resume, leap seconds, etc */ + start_timezone_timer (self); + return G_SOURCE_REMOVE; +} + static void -update_header_action (IndicatorDatetimeService * self) +start_timezone_timer (IndicatorDatetimeService * self) { - GVariant * v; - gchar * a11y = g_strdup ("a11y"); - const gchar * label = "Hello World"; - const gchar * iconstr = "icon"; - const priv_t * const p = self->priv; + priv_t * p = self->priv; - g_return_if_fail (p->header_action != NULL); + indicator_clear_timer (&p->timezone_timer); - v = g_variant_new ("(sssb)", label, iconstr, a11y, TRUE); - g_simple_action_set_state (p->header_action, v); - g_free (a11y); + p->timezone_timer = g_timeout_add_seconds (calculate_seconds_until_next_fifteen_minutes(), + on_timezone_timer, + self); } /*** +**** HEADER TIMER +***/ + +/* + * This is to periodically rebuild the header's action's state. + * + * If the label shows seconds, update when we reach the next second. + * Otherwise, update when we reach the next minute. + */ + +static guint +calculate_milliseconds_until_next_minute (void) +{ + GDateTime * now; + GDateTime * next; + GDateTime * start_of_next; + GTimeSpan interval_usec; + guint interval_msec; + + now = g_date_time_new_now_local (); + next = g_date_time_add_minutes (now, 1); + start_of_next = g_date_time_new_local (g_date_time_get_year (next), + g_date_time_get_month (next), + g_date_time_get_day_of_month (next), + g_date_time_get_hour (next), + g_date_time_get_minute (next), + 0); + + interval_usec = g_date_time_difference (start_of_next, now); + interval_msec = (interval_usec + 999) / 1000; + + g_date_time_unref (start_of_next); + g_date_time_unref (next); + g_date_time_unref (now); + + return interval_msec; +} + +static gint +calculate_milliseconds_until_next_second (void) +{ + GDateTime * now; + gint interval_usec; + guint interval_msec; + + now = g_date_time_new_now_local (); + + interval_usec = G_USEC_PER_SEC - g_date_time_get_microsecond (now); + interval_msec = (interval_usec + 999) / 1000; + + g_date_time_unref (now); + + return interval_msec; +} + +static void start_header_timer (IndicatorDatetimeService * self); + +static gboolean +on_header_timer (gpointer gself) +{ + IndicatorDatetimeService * self = INDICATOR_DATETIME_SERVICE (gself); + + rebuild_now (self, SECTION_HEADER); + + /* Restarting the timer to recalculate the interval. This helps us to hit + our marks despite clock skew, suspend+resume, leap seconds, etc */ + start_header_timer (self); + return G_SOURCE_REMOVE; +} + +static char * get_header_label_format_string (IndicatorDatetimeService *); + +static void +start_header_timer (IndicatorDatetimeService * self) +{ + guint interval_msec; + gboolean header_shows_seconds = FALSE; + priv_t * p = self->priv; + + indicator_clear_timer (&p->header_timer); + + if (g_settings_get_boolean (self->priv->settings, SETTINGS_SHOW_CLOCK_S)) + { + char * fmt = get_header_label_format_string (self); + header_shows_seconds = fmt && (strstr(fmt,"%s") || strstr(fmt,"%S") || + strstr(fmt,"%T") || strstr(fmt,"%X") || + strstr(fmt,"%c")); + g_free (fmt); + } + + if (header_shows_seconds) + interval_msec = calculate_milliseconds_until_next_second (); + else + interval_msec = calculate_milliseconds_until_next_minute (); + + interval_msec += 50; /* add a small margin to ensure the callback + fires /after/ next is reached */ + + p->header_timer = g_timeout_add_full (G_PRIORITY_HIGH, + interval_msec, + on_header_timer, + self, + NULL); +} + +/** + * General purpose handler for rebuilding sections and restarting their timers + * when time jumps for whatever reason: + * + * - clock skew + * - laptop suspend + resume + * - geoclue detects that we've changed timezones + * - Unity is running inside a TARDIS + */ +static void +on_local_time_jumped (IndicatorDatetimeService * self) +{ + g_debug ("%s %s", G_STRLOC, G_STRFUNC); + + /* these calls accomplish two things: + 1. rebuild the necessary states / menuitems when time jumps + 2. restart the timers so their new wait interval is correct */ + + on_header_timer (self); + on_timezone_timer (self); +} + +static gboolean +skew_timer_func (gpointer gself) +{ + GDateTime * now = g_date_time_new_now_local (); + IndicatorDatetimeService * self = INDICATOR_DATETIME_SERVICE (gself); + priv_t * p = self->priv; + + /* check for clock skew: has too much time passed since the last check? */ + if (p->skew_time != NULL) + { + const GTimeSpan diff = g_date_time_difference (now, p->skew_time); + + if (diff > SKEW_DIFF_THRESHOLD_USEC) + on_local_time_jumped (self); + } + + g_clear_pointer (&p->skew_time, g_date_time_unref); + p->skew_time = now; + return G_SOURCE_CONTINUE; +} + +/*** +**** +**** HEADER SECTION **** ***/ -static GMenuModel * -create_calendar_section (IndicatorDatetimeService * self G_GNUC_UNUSED) +typedef enum { - GMenu * menu; + TIME_FORMAT_MODE_LOCALE_DEFAULT, + TIME_FORMAT_MODE_12_HOUR, + TIME_FORMAT_MODE_24_HOUR, + TIME_FORMAT_MODE_CUSTOM +} +TimeFormatMode; - menu = g_menu_new (); +/* gets the user's time-format from GSettings */ +static TimeFormatMode +get_time_format_mode (IndicatorDatetimeService * self) +{ + char * str; + TimeFormatMode mode; + + str = g_settings_get_string (self->priv->settings, SETTINGS_TIME_FORMAT_S); + + if (!g_strcmp0 ("12-hour", str)) + mode = TIME_FORMAT_MODE_12_HOUR; + else if (!g_strcmp0 ("24-hour", str)) + mode = TIME_FORMAT_MODE_24_HOUR; + else if (!g_strcmp0 ("custom", str)) + mode = TIME_FORMAT_MODE_CUSTOM; + else + mode = TIME_FORMAT_MODE_LOCALE_DEFAULT; + + g_free (str); + return mode; +} + +static gchar * +get_header_label_format_string (IndicatorDatetimeService * self) +{ + char * fmt; + const TimeFormatMode mode = get_time_format_mode (self); + GSettings * s = self->priv->settings; + + if (mode == TIME_FORMAT_MODE_CUSTOM) + { + fmt = g_settings_get_string (s, SETTINGS_CUSTOM_TIME_FORMAT_S); + } + else + { + gboolean show_day = g_settings_get_boolean (s, SETTINGS_SHOW_DAY_S); + gboolean show_date = g_settings_get_boolean (s, SETTINGS_SHOW_DATE_S); + fmt = generate_format_string_full (show_day, show_date); + } + + return fmt; +} + +static GVariant * +create_header_state (IndicatorDatetimeService * self) +{ + GVariantBuilder b; + gchar * fmt; + gchar * str; + gboolean visible; + GDateTime * now_local; + priv_t * p = self->priv; + + visible = g_settings_get_boolean (p->settings, SETTINGS_SHOW_CLOCK_S); + + /* build the time string for the label & a11y */ + fmt = get_header_label_format_string (self); + now_local = g_date_time_new_now_local (); + str = g_date_time_format (now_local, fmt); + if (str == NULL) + { + str = g_strdup (_("Unsupported date format")); + g_warning ("%s", str); + } + + g_variant_builder_init (&b, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add (&b, "{sv}", "accessible-desc", g_variant_new_string (str)); + g_variant_builder_add (&b, "{sv}", "label", g_variant_new_string (str)); + g_variant_builder_add (&b, "{sv}", "visible", g_variant_new_boolean (visible)); + + /* cleanup */ + g_date_time_unref (now_local); + g_free (str); + g_free (fmt); + return g_variant_builder_end (&b); +} + + +/*** +**** +**** CALENDAR SECTION +**** +***/ + +static GDateTime * +get_calendar_date (IndicatorDatetimeService * self) +{ + GDateTime * date; + priv_t * p = self->priv; + + if (p->calendar_date == 0) + date = g_date_time_new_now_local (); + else + date = g_date_time_new_from_unix_local ((gint64)p->calendar_date); + + return date; +} + +static GSList * +get_all_appointments_this_month (IndicatorDatetimeService * self) +{ + GSList * appointments = NULL; + priv_t * p = self->priv; + + if (p->planner != NULL) + { + GDateTime * calendar_date; + GDateTime * begin; + GDateTime * end; + int y, m, d; + + calendar_date = get_calendar_date (self); + g_date_time_get_ymd (calendar_date, &y, &m, &d); + begin = g_date_time_new_local (y, m, 0, + 0, 0, 0); + end = g_date_time_new_local (y, m, g_date_get_days_in_month(m,y), + 23, 59, 0); + + appointments = indicator_datetime_planner_get_appointments (p->planner, + begin, + end); + + g_date_time_unref (end); + g_date_time_unref (begin); + g_date_time_unref (calendar_date); + } + + return appointments; +} + +static GVariant * +create_calendar_state (IndicatorDatetimeService * self) +{ + guint i; + const char * key; + gboolean days[32] = { 0 }; + GVariantBuilder dict_builder; + GVariantBuilder day_builder; + GDateTime * date; + GSList * l; + GSList * appts; + gboolean b; + priv_t * p = self->priv; + + g_variant_builder_init (&dict_builder, G_VARIANT_TYPE_DICTIONARY); + + key = "appointment-days"; + appts = get_all_appointments_this_month (self); + for (l=appts; l!=NULL; l=l->next) + { + const struct IndicatorDatetimeAppt * appt = l->data; + days[g_date_time_get_day_of_month (appt->begin)] = TRUE; + } + g_variant_builder_init (&day_builder, G_VARIANT_TYPE("ai")); + for (i=0; i<G_N_ELEMENTS(days); i++) + if (days[i]) + g_variant_builder_add (&day_builder, "i", i); + g_variant_builder_add (&dict_builder, "{sv}", key, + g_variant_builder_end (&day_builder)); + g_slist_free_full (appts, (GDestroyNotify)indicator_datetime_appt_free); + + key = "calendar-day"; + date = get_calendar_date (self); + g_variant_builder_add (&dict_builder, "{sv}", key, + g_variant_new_int64 (g_date_time_to_unix (date))); + g_date_time_unref (date); + + key = "show-week-numbers"; + b = g_settings_get_boolean (p->settings, SETTINGS_SHOW_WEEK_NUMBERS_S); + g_variant_builder_add (&dict_builder, "{sv}", key, g_variant_new_boolean (b)); + + return g_variant_builder_end (&dict_builder); +} + +static void +update_calendar_action_state (IndicatorDatetimeService * self) +{ + g_simple_action_set_state (self->priv->calendar_action, + create_calendar_state (self)); +} + +static GMenuModel * +create_calendar_section (IndicatorDatetimeService * self) +{ + char * label; + GMenuItem * menu_item; + GDateTime * date_time; + GMenu * menu = g_menu_new (); + + /* create the local date menuitem */ + date_time = g_date_time_new_now_local (); + if (date_time == NULL) + { + label = g_strdup (_("Error getting time")); + } + else + { + label = g_date_time_format (date_time, _("%A, %e %B %Y")); + g_date_time_unref (date_time); + } + menu_item = g_menu_item_new (label, NULL); + g_menu_item_set_action_and_target_value (menu_item, "indicator.activate-planner", + g_variant_new_int64(0)); + g_menu_append_item (menu, menu_item); + g_object_unref (menu_item); + g_free (label); + + /* create the calendar menuitem */ + if (g_settings_get_boolean (self->priv->settings, SETTINGS_SHOW_CALENDAR_S)) + { + label = g_strdup ("[calendar]"); + menu_item = g_menu_item_new ("[calendar]", NULL); + g_menu_item_set_action_and_target_value (menu_item, + "indicator.calendar", + g_variant_new_int64(0)); + g_menu_item_set_attribute (menu_item, "x-canonical-type", + "s", "com.canonical.indicator.calendar"); + g_menu_item_set_attribute (menu_item, "activation-action", + "s", "indicator.activate-planner"); + g_menu_append_item (menu, menu_item); + g_object_unref (menu_item); + g_free (label); + } return G_MENU_MODEL (menu); } +/*** +**** +**** APPOINTMENTS SECTION +**** +***/ + +/* gets the next MAX_APPTS appointments */ +static GSList * +get_upcoming_appointments (IndicatorDatetimeService * self) +{ + const int MAX_APPTS = 5; + GSList * l; + GSList * appts = NULL; + priv_t * p = self->priv; + + if (p->planner != NULL) + { + GDateTime * begin = get_calendar_date (self); + GDateTime * end = g_date_time_add_months (begin, 1); + + appts = indicator_datetime_planner_get_appointments (p->planner, + begin, + end); + + g_date_time_unref (end); + g_date_time_unref (begin); + } + + /* truncate at MAX_APPTS */ + if ((l = g_slist_nth (appts, MAX_APPTS-1))) + { + g_slist_free_full (l->next, (GDestroyNotify)indicator_datetime_appt_free); + l->next = NULL; + } + + return appts; +} + +static char * +get_appointment_time_format (struct IndicatorDatetimeAppt * appt) +{ + char * fmt; + gboolean full_day = g_date_time_difference (appt->end, appt->begin) == G_TIME_SPAN_DAY; + + if (full_day) + { + /* TRANSLATORS: This is a strftime string for the day for full day events + in the menu. It should most likely be either '%A' for a full text day + (Wednesday) or '%a' for a shortened one (Wed). You should only need to + change for '%a' in the case of langauges with very long day names. */ + fmt = g_strdup (_("%A")); + } + else + { + fmt = generate_format_string_at_time (appt->begin); + } + + return fmt; +} + static GMenuModel * -create_appointments_section (IndicatorDatetimeService * self G_GNUC_UNUSED) +create_appointments_section (IndicatorDatetimeService * self) { - GMenu * menu; + priv_t * p = self->priv; + GMenu * menu = g_menu_new (); - menu = g_menu_new (); + if (g_settings_get_boolean (p->settings, SETTINGS_SHOW_EVENTS_S)) + { + GSList * l; + GSList * appts; + GMenuItem * menu_item; + + /* build appointment menuitems */ + appts = get_upcoming_appointments (self); + for (l=appts; l!=NULL; l=l->next) + { + struct IndicatorDatetimeAppt * appt = l->data; + char * fmt = get_appointment_time_format (appt); + const gint64 unix_time = g_date_time_to_unix (appt->begin); + + menu_item = g_menu_item_new (appt->summary, NULL); + g_menu_item_set_attribute (menu_item, "x-canonical-color", + "s", appt->color); + g_menu_item_set_attribute (menu_item, "x-canonical-time", + "x", unix_time); + g_menu_item_set_attribute (menu_item, "x-canonical-time-format", + "s", fmt); + g_menu_item_set_attribute (menu_item, "x-canonical-type", + "s", "com.canonical.indicator.appointment"); + g_menu_item_set_action_and_target_value (menu_item, + "indicator.activate-planner", + g_variant_new_int64 (unix_time)); + g_menu_append_item (menu, menu_item); + g_object_unref (menu_item); + g_free (fmt); + } + + /* build 'add event' menuitem */ + menu_item = g_menu_item_new (_("Add Event…"), NULL); + g_menu_item_set_action_and_target_value (menu_item, + "indicator.activate-planner", + g_variant_new_int64 (0)); + g_menu_append_item (menu, menu_item); + g_object_unref (menu_item); + + /* cleanup */ + g_slist_free_full (appts, (GDestroyNotify)indicator_datetime_appt_free); + } return G_MENU_MODEL (menu); } + +/*** +**** +**** LOCATIONS SECTION +**** +***/ + +static void +on_current_timezone_changed (IndicatorDatetimeService * self) +{ + on_local_time_jumped (self); +} + +/* When the 'auto-detect timezone' boolean setting changes, + start or stop watching geoclue and /etc/timezone */ +static void +set_detect_location_enabled (IndicatorDatetimeService * self, gboolean enabled) +{ + gboolean changed = FALSE; + priv_t * p = self->priv; + + /* geoclue */ + + if (!p->tz_geoclue && enabled) + { + p->tz_geoclue = indicator_datetime_timezone_geoclue_new (); + g_signal_connect_swapped (p->tz_geoclue, "notify::timezone", + G_CALLBACK(on_current_timezone_changed), + self); + changed = TRUE; + } + else if (p->tz_geoclue && !enabled) + { + g_signal_handlers_disconnect_by_func (p->tz_geoclue, + on_current_timezone_changed, + self); + g_clear_object (&p->tz_geoclue); + changed = TRUE; + } + + /* timezone file */ + + if (!p->tz_file && enabled) + { + p->tz_file = indicator_datetime_timezone_file_new (TIMEZONE_FILE); + g_signal_connect_swapped (p->tz_file, "notify::timezone", + G_CALLBACK(on_current_timezone_changed), + self); + changed = TRUE; + } + else if (p->tz_file && !enabled) + { + g_signal_handlers_disconnect_by_func (p->tz_file, + on_current_timezone_changed, + self); + g_clear_object (&p->tz_file); + changed = TRUE; + } + + if (changed) + on_current_timezone_changed (self); +} + +/* A temp struct used by create_locations_section() + for pruning duplicates and sorting. */ +struct TimeLocation +{ + gint32 offset; + gchar * zone; + gchar * name; + gboolean visible; + GDateTime * local_time; +}; + +static void +time_location_free (struct TimeLocation * loc) +{ + g_date_time_unref (loc->local_time); + g_free (loc->name); + g_free (loc->zone); + g_free (loc); +} + +static struct TimeLocation* +time_location_new (const char * zone, + const char * name, + gboolean visible, + time_t now) +{ + struct TimeLocation * loc = g_new (struct TimeLocation, 1); + GTimeZone * tz = g_time_zone_new (zone); + gint interval = g_time_zone_find_interval (tz, G_TIME_TYPE_UNIVERSAL, now); + loc->offset = g_time_zone_get_offset (tz, interval); + loc->zone = g_strdup (zone); + loc->name = g_strdup (name); + loc->visible = visible; + loc->local_time = g_date_time_new_now (tz); + g_time_zone_unref (tz); + return loc; +} + +static int +time_location_compare (const struct TimeLocation * a, + const struct TimeLocation * b) +{ + int ret = a->offset - b->offset; /* primary key */ + + if (!ret) + ret = g_strcmp0 (a->name, b->name); /* secondary key */ + + if (!ret) + ret = a->visible - b->visible; /* tertiary key */ + + return ret; +} + +static GSList* +locations_add (GSList * locations, + const char * zone, + const char * name, + gboolean visible, + time_t now) +{ + struct TimeLocation * loc = time_location_new (zone, name, visible, now); + + if (g_slist_find_custom (locations, loc, (GCompareFunc)time_location_compare)) + { + g_debug("%s Skipping duplicate zone '%s' name '%s'", G_STRLOC, zone, name); + time_location_free (loc); + } + else + { + g_debug ("%s Adding zone '%s', name '%s'", G_STRLOC, zone, name); + locations = g_slist_append (locations, loc); + } + + return locations; +} + static GMenuModel * -create_locations_section (IndicatorDatetimeService * self G_GNUC_UNUSED) +create_locations_section (IndicatorDatetimeService * self) { + guint i; GMenu * menu; + GSList * l; + GSList * locations = NULL; + gchar ** user_locations; + gboolean visible; + const time_t now = time (NULL); + priv_t * p = self->priv; + IndicatorDatetimeTimezone * detected_timezones[2]; + + set_detect_location_enabled (self, + g_settings_get_boolean (p->settings, SETTINGS_SHOW_DETECTED_S)); menu = g_menu_new (); + /*** + **** Build a list of locations to add: use geo_timezone, + **** current_timezone, and SETTINGS_LOCATIONS_S, but omit duplicates. + ***/ + + /* maybe add the auto-detected timezones */ + detected_timezones[0] = p->tz_geoclue; + detected_timezones[1] = p->tz_file; + visible = g_settings_get_boolean (p->settings, SETTINGS_SHOW_DETECTED_S); + for (i=0; i<G_N_ELEMENTS(detected_timezones); i++) + { + if (detected_timezones[i] != NULL) + { + const char * tz = indicator_datetime_timezone_get_timezone (detected_timezones[i]); + if (tz && *tz) + { + gchar * name = get_current_zone_name (tz); + locations = locations_add (locations, tz, name, visible, now); + g_free (name); + } + } + } + + /* maybe add the user-specified locations */ + user_locations = g_settings_get_strv (p->settings, SETTINGS_LOCATIONS_S); + if (user_locations != NULL) + { + visible = g_settings_get_boolean (p->settings, SETTINGS_SHOW_LOCATIONS_S); + + for (i=0; user_locations[i] != NULL; i++) + { + gchar * zone; + gchar * name; + split_settings_location (user_locations[i], &zone, &name); + locations = locations_add (locations, zone, name, visible, now); + g_free (name); + g_free (zone); + } + + g_strfreev (user_locations); + user_locations = NULL; + } + + /* now build menuitems for all the locations */ + for (l=locations; l!=NULL; l=l->next) + { + struct TimeLocation * loc = l->data; + if (loc->visible) + { + char * label; + char * detailed_action; + char * fmt; + GMenuItem * menu_item; + + label = g_strdup (loc->name); + detailed_action = g_strdup_printf ("indicator.set-location::%s %s", + loc->zone, + loc->name); + fmt = generate_format_string_at_time (loc->local_time); + + menu_item = g_menu_item_new (label, detailed_action); + g_menu_item_set_attribute (menu_item, "x-canonical-type", + "s", "com.canonical.indicator.location"); + g_menu_item_set_attribute (menu_item, "x-canonical-timezone", + "s", loc->zone); + g_menu_item_set_attribute (menu_item, "x-canonical-time-format", + "s", fmt); + g_menu_append_item (menu, menu_item); + + g_object_unref (menu_item); + g_free (fmt); + g_free (detailed_action); + g_free (label); + } + } + + g_slist_free_full (locations, (GDestroyNotify)time_location_free); return G_MENU_MODEL (menu); } -static GMenuModel * -create_settings_section (IndicatorDatetimeService * self G_GNUC_UNUSED) +/*** +**** +***/ + +struct settimezone_data { - GMenu * menu; + IndicatorDatetimeService * service; + char * timezone_id; + char * name; +}; - menu = g_menu_new (); +static void +settimezone_data_free (struct settimezone_data * data) +{ + g_free (data->timezone_id); + g_free (data->name); + g_free (data); +} - g_menu_append (menu, _("Date and Time Settings\342\200\246"), "indicator.activateSettings"); +static void +on_datetime1_set_timezone_response (GObject * object, + GAsyncResult * res, + gpointer gdata) +{ + GError * err; + GVariant * answers; + struct settimezone_data * data = gdata; + + err = NULL; + answers = g_dbus_proxy_call_finish (G_DBUS_PROXY(object), res, &err); + if (err != NULL) + { + g_warning ("Could not set new timezone: %s", err->message); + g_error_free (err); + } + else + { + char * timezone_name = g_strdup_printf ("%s %s", + data->timezone_id, + data->name); + + g_settings_set_string (data->service->priv->settings, + SETTINGS_TIMEZONE_NAME_S, + timezone_name); + + g_free (timezone_name); + g_variant_unref (answers); + } + + settimezone_data_free (data); +} + +static void +on_datetime1_proxy_ready (GObject * object G_GNUC_UNUSED, + GAsyncResult * res, + gpointer gdata) +{ + GError * err; + GDBusProxy * proxy; + struct settimezone_data * data = gdata; + + err = NULL; + proxy = g_dbus_proxy_new_for_bus_finish (res, &err); + if (err != NULL) + { + g_warning ("Could not grab DBus proxy for timedated: %s", err->message); + g_error_free (err); + settimezone_data_free (data); + } + else + { + g_dbus_proxy_call (proxy, + "SetTimezone", + g_variant_new ("(sb)", data->timezone_id, TRUE), + G_DBUS_CALL_FLAGS_NONE, + -1, + data->service->priv->cancellable, + on_datetime1_set_timezone_response, + data); + + g_object_unref (proxy); + } +} + +static void +indicator_datetime_service_set_location (IndicatorDatetimeService * self, + const char * timezone_id, + const char * name) +{ + priv_t * p = self->priv; + struct settimezone_data * data; + + g_return_if_fail (INDICATOR_IS_DATETIME_SERVICE (self)); + g_return_if_fail (name && *name); + g_return_if_fail (timezone_id && *timezone_id); + + data = g_new0 (struct settimezone_data, 1); + data->timezone_id = g_strdup (timezone_id); + data->name = g_strdup (name); + data->service = self; + + g_dbus_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "org.freedesktop.timedate1", + "/org/freedesktop/timedate1", + "org.freedesktop.timedate1", + p->cancellable, + on_datetime1_proxy_ready, + data); +} + +static void +on_set_location (GSimpleAction * a G_GNUC_UNUSED, + GVariant * param, + gpointer gself) +{ + char * zone; + char * name; + IndicatorDatetimeService * self; + + self = INDICATOR_DATETIME_SERVICE (gself); + split_settings_location (g_variant_get_string (param, NULL), &zone, &name); + indicator_datetime_service_set_location (self, zone, name); + + g_free (name); + g_free (zone); +} +/*** +**** +***/ + +static GMenuModel * +create_settings_section (IndicatorDatetimeService * self G_GNUC_UNUSED) +{ + GMenu * menu = g_menu_new (); + g_menu_append (menu, _("Date and Time Settings\342\200\246"), "indicator.activate-settings"); return G_MENU_MODEL (menu); } @@ -221,11 +1179,13 @@ create_menu (IndicatorDatetimeService * self, int profile) } else if (profile == PROFILE_GREETER) { - /* FIXME: what goes here? */ + sections[n++] = create_calendar_section (self); } /* add sections to the submenu */ + submenu = g_menu_new (); + for (i=0; i<n; ++i) { g_menu_append_section (submenu, NULL, sections[i]); @@ -234,7 +1194,8 @@ create_menu (IndicatorDatetimeService * self, int profile) /* add submenu to the header */ header = g_menu_item_new (NULL, "indicator._header"); - g_menu_item_set_attribute (header, "x-canonical-type", "s", "com.canonical.indicator.root"); + g_menu_item_set_attribute (header, "x-canonical-type", + "s", "com.canonical.indicator.root"); g_menu_item_set_submenu (header, G_MENU_MODEL (submenu)); g_object_unref (submenu); @@ -251,23 +1212,87 @@ create_menu (IndicatorDatetimeService * self, int profile) **** GActions ***/ +/* Run a particular program based on an activation */ +static void +execute_command (const gchar * cmd) +{ + GError * err = NULL; + + g_debug ("Issuing command '%s'", cmd); + + if (!g_spawn_command_line_async (cmd, &err)) + { + g_warning ("Unable to start %s: %s", cmd, err->message); + g_error_free (err); + } +} + +#ifdef HAVE_CCPANEL + #define SETTINGS_APP_INVOCATION "gnome-control-center indicator-datetime" +#else + #define SETTINGS_APP_INVOCATION "gnome-control-center datetime" +#endif + static void on_settings_activated (GSimpleAction * a G_GNUC_UNUSED, GVariant * param G_GNUC_UNUSED, gpointer gself G_GNUC_UNUSED) { - g_message ("settings activated"); + execute_command (SETTINGS_APP_INVOCATION); } static void +on_activate_planner (GSimpleAction * a G_GNUC_UNUSED, + GVariant * param, + gpointer gself) +{ + priv_t * p = INDICATOR_DATETIME_SERVICE(gself)->priv; + + if (p->planner != NULL) + { + const time_t t = g_variant_get_int64 (param); + + if (t) + { + GDateTime * date_time = g_date_time_new_from_unix_local (t); + indicator_datetime_planner_activate_time (p->planner, date_time); + g_date_time_unref (date_time); + } + else /* no time specified... */ + { + indicator_datetime_planner_activate (p->planner); + } + } +} + +static void +on_calendar_action_activated (GSimpleAction * action G_GNUC_UNUSED, + GVariant * state, + gpointer gself) +{ + IndicatorDatetimeService * self = INDICATOR_DATETIME_SERVICE (gself); + priv_t * p = self->priv; + const time_t calendar_date = (time_t) g_variant_get_int64 (state); + + if (p->calendar_date != calendar_date) + { + p->calendar_date = (time_t) g_variant_get_int64 (state); + update_calendar_action_state (self); + rebuild_appointments_section_soon (self); + } +} + + +static void init_gactions (IndicatorDatetimeService * self) { - GVariant * v; GSimpleAction * a; priv_t * p = self->priv; GActionEntry entries[] = { - { "activateSettings", on_settings_activated, NULL, NULL, NULL }, + { "activate-settings", on_settings_activated }, + { "activate-planner", on_activate_planner, "x", NULL }, + { "set-location", on_set_location, "s" } }; p->actions = g_simple_action_group_new (); @@ -278,11 +1303,19 @@ init_gactions (IndicatorDatetimeService * self) self); /* add the header action */ - v = g_variant_new ("(sssb)", "Hello World", "icon", "a11y", TRUE); - a = g_simple_action_new_stateful ("_header", NULL, v); + a = g_simple_action_new_stateful ("_header", NULL, create_header_state (self)); g_simple_action_group_insert (p->actions, G_ACTION(a)); p->header_action = a; + /* add the calendar action */ + a = g_simple_action_new_stateful ("calendar", + G_VARIANT_TYPE_INT64, + create_calendar_state (self)); + g_simple_action_group_insert (p->actions, G_ACTION(a)); + g_signal_connect (a, "activate", + G_CALLBACK(on_calendar_action_activated), self); + p->calendar_action = a; + rebuild_now (self, SECTION_HEADER); } @@ -308,16 +1341,17 @@ rebuild_now (IndicatorDatetimeService * self, int sections) { priv_t * p = self->priv; struct ProfileMenuInfo * desktop = &p->menus[PROFILE_DESKTOP]; - //struct ProfileMenuInfo * greeter = &p->menus[PROFILE_GREETER]; + struct ProfileMenuInfo * greeter = &p->menus[PROFILE_GREETER]; if (sections & SECTION_HEADER) { - update_header_action (self); + g_simple_action_set_state (p->header_action, create_header_state (self)); } if (sections & SECTION_CALENDAR) { rebuild_section (desktop->submenu, 0, create_calendar_section (self)); + rebuild_section (greeter->submenu, 0, create_calendar_section (self)); } if (sections & SECTION_APPOINTMENTS) @@ -333,7 +1367,6 @@ rebuild_now (IndicatorDatetimeService * self, int sections) if (sections & SECTION_SETTINGS) { rebuild_section (desktop->submenu, 3, create_settings_section (self)); - //rebuild_section (greeter->submenu, 0, create_datetime_section(self)); } } @@ -369,7 +1402,52 @@ rebuild_soon (IndicatorDatetimeService * self, int section) } /*** -**** GDBus +**** org.freedesktop.login1.Manager +***/ + +static void +on_login1_manager_signal (GDBusProxy * proxy G_GNUC_UNUSED, + gchar * sender_name G_GNUC_UNUSED, + gchar * signal_name, + GVariant * parameters, + gpointer gself) +{ + if (!g_strcmp0 (signal_name, "PrepareForSleep")) + { + gboolean sleeping = FALSE; + g_variant_get (parameters, "(b)", &sleeping); + if (!sleeping) + on_local_time_jumped (INDICATOR_DATETIME_SERVICE (gself)); + } +} + +static void +on_login1_manager_proxy_ready (GObject * object G_GNUC_UNUSED, + GAsyncResult * res, + gpointer gself) +{ + GError * err; + GDBusProxy * proxy; + + err = NULL; + proxy = g_dbus_proxy_new_for_bus_finish (res, &err); + + if (err != NULL) + { + g_warning ("Could not grab DBus proxy for logind: %s", err->message); + g_error_free (err); + } + else + { + IndicatorDatetimeService * self = INDICATOR_DATETIME_SERVICE (gself); + self->priv->login1_manager = proxy; + g_signal_connect (proxy, "g-signal", + G_CALLBACK(on_login1_manager_signal), self); + } +} + +/*** +**** GDBus ***/ static void @@ -464,9 +1542,10 @@ on_name_lost (GDBusConnection * connection G_GNUC_UNUSED, unexport (self); - g_signal_emit (self, signals[NAME_LOST], 0, NULL); + g_signal_emit (self, signals[SIGNAL_NAME_LOST], 0, NULL); } + /*** **** GObject virtual functions ***/ @@ -476,21 +1555,22 @@ my_constructed (GObject * o) { GBusNameOwnerFlags owner_flags; IndicatorDatetimeService * self = INDICATOR_DATETIME_SERVICE(o); + priv_t * p = self->priv; /* own the name in constructed() instead of init() so that we'll know the value of the 'replace' property */ owner_flags = G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT; - if (self->priv->replace) + if (p->replace) owner_flags |= G_BUS_NAME_OWNER_FLAGS_REPLACE; - self->priv->own_id = g_bus_own_name (G_BUS_TYPE_SESSION, - BUS_NAME, - owner_flags, - on_bus_acquired, - NULL, - on_name_lost, - self, - NULL); + p->own_id = g_bus_own_name (G_BUS_TYPE_SESSION, + BUS_NAME, + owner_flags, + on_bus_acquired, + NULL, + on_name_lost, + self, + NULL); } static void @@ -552,10 +1632,29 @@ my_dispose (GObject * o) g_clear_object (&p->cancellable); } - if (p->rebuild_id) + set_detect_location_enabled (self, FALSE); + + if (p->planner != NULL) { - g_source_remove (p->rebuild_id); - p->rebuild_id = 0; + g_signal_handlers_disconnect_by_data (p->planner, self); + g_clear_object (&p->planner); + } + + if (p->login1_manager != NULL) + { + g_signal_handlers_disconnect_by_data (p->login1_manager, self); + g_clear_object (&p->login1_manager); + } + + indicator_clear_timer (&p->skew_timer); + indicator_clear_timer (&p->rebuild_id); + indicator_clear_timer (&p->timezone_timer); + indicator_clear_timer (&p->header_timer); + + if (p->settings != NULL) + { + g_signal_handlers_disconnect_by_data (p->settings, self); + g_clear_object (&p->settings); } g_clear_object (&p->actions); @@ -563,12 +1662,25 @@ my_dispose (GObject * o) for (i=0; i<N_PROFILES; ++i) g_clear_object (&p->menus[i].menu); + g_clear_object (&p->planner); + g_clear_object (&p->calendar_action); g_clear_object (&p->header_action); g_clear_object (&p->conn); G_OBJECT_CLASS (indicator_datetime_service_parent_class)->dispose (o); } +static void +my_finalize (GObject * o) +{ + IndicatorDatetimeService * self = INDICATOR_DATETIME_SERVICE(o); + priv_t * p = self->priv; + + g_clear_pointer (&p->skew_time, g_date_time_unref); + + G_OBJECT_CLASS (indicator_datetime_service_parent_class)->dispose (o); +} + /*** **** Instantiation ***/ @@ -576,18 +1688,125 @@ my_dispose (GObject * o) static void indicator_datetime_service_init (IndicatorDatetimeService * self) { + guint i, n; priv_t * p; + GString * gstr = g_string_new (NULL); + + /* these are the settings that affect the + contents of the respective sections */ + const char * const header_settings[] = { + SETTINGS_SHOW_CLOCK_S, + SETTINGS_TIME_FORMAT_S, + SETTINGS_SHOW_SECONDS_S, + SETTINGS_SHOW_DAY_S, + SETTINGS_SHOW_DATE_S, + SETTINGS_CUSTOM_TIME_FORMAT_S + }; + const char * const calendar_settings[] = { + SETTINGS_SHOW_CALENDAR_S, + SETTINGS_SHOW_WEEK_NUMBERS_S + }; + const char * const appointment_settings[] = { + SETTINGS_SHOW_EVENTS_S, + SETTINGS_TIME_FORMAT_S, + SETTINGS_SHOW_SECONDS_S + }; + const char * const location_settings[] = { + SETTINGS_TIME_FORMAT_S, + SETTINGS_SHOW_SECONDS_S, + SETTINGS_CUSTOM_TIME_FORMAT_S, + SETTINGS_SHOW_LOCATIONS_S, + SETTINGS_LOCATIONS_S, + SETTINGS_SHOW_DETECTED_S, + SETTINGS_TIMEZONE_NAME_S + }; + const char * const time_format_string_settings[] = { + SETTINGS_TIME_FORMAT_S, + SETTINGS_SHOW_SECONDS_S, + SETTINGS_CUSTOM_TIME_FORMAT_S + }; + + + /* init the priv pointer */ - /* init our priv pointer */ p = G_TYPE_INSTANCE_GET_PRIVATE (self, INDICATOR_TYPE_DATETIME_SERVICE, IndicatorDatetimeServicePrivate); self->priv = p; - /* init the backend objects */ p->cancellable = g_cancellable_new (); + /*** + **** Create the planner and listen for changes + ***/ + + p->planner = indicator_datetime_planner_eds_new (); + + g_signal_connect_swapped (p->planner, "appointments-changed", + G_CALLBACK(rebuild_calendar_section_soon), self); + + + /*** + **** Create the settings object and listen for changes + ***/ + + p->settings = g_settings_new (SETTINGS_INTERFACE); + for (i=0, n=G_N_ELEMENTS(header_settings); i<n; i++) + { + g_string_printf (gstr, "changed::%s", header_settings[i]); + g_signal_connect_swapped (p->settings, gstr->str, + G_CALLBACK(rebuild_header_soon), self); + } + + for (i=0, n=G_N_ELEMENTS(calendar_settings); i<n; i++) + { + g_string_printf (gstr, "changed::%s", calendar_settings[i]); + g_signal_connect_swapped (p->settings, gstr->str, + G_CALLBACK(rebuild_calendar_section_soon), self); + } + + for (i=0, n=G_N_ELEMENTS(appointment_settings); i<n; i++) + { + g_string_printf (gstr, "changed::%s", appointment_settings[i]); + g_signal_connect_swapped (p->settings, gstr->str, + G_CALLBACK(rebuild_appointments_section_soon), self); + } + + for (i=0, n=G_N_ELEMENTS(location_settings); i<n; i++) + { + g_string_printf (gstr, "changed::%s", location_settings[i]); + g_signal_connect_swapped (p->settings, gstr->str, + G_CALLBACK(rebuild_locations_section_soon), self); + } + + /* The keys in time_format_string_settings affect the time format strings we build. + When these change, we need to rebuild everything that has a time format string. */ + for (i=0, n=G_N_ELEMENTS(time_format_string_settings); i<n; i++) + { + g_string_printf (gstr, "changed::%s", time_format_string_settings[i]); + g_signal_connect_swapped (p->settings, gstr->str, + G_CALLBACK(on_local_time_jumped), self); + } + init_gactions (self); + + g_dbus_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "org.freedesktop.login1", + "/org/freedesktop/login1", + "org.freedesktop.login1.Manager", + p->cancellable, + on_login1_manager_proxy_ready, + self); + + p->skew_timer = g_timeout_add_seconds (SKEW_CHECK_INTERVAL_SEC, + skew_timer_func, + self); + + on_local_time_jumped (self); + + g_string_free (gstr, TRUE); } static void @@ -596,19 +1815,21 @@ indicator_datetime_service_class_init (IndicatorDatetimeServiceClass * klass) GObjectClass * object_class = G_OBJECT_CLASS (klass); object_class->dispose = my_dispose; + object_class->finalize = my_finalize; object_class->constructed = my_constructed; object_class->get_property = my_get_property; object_class->set_property = my_set_property; g_type_class_add_private (klass, sizeof (IndicatorDatetimeServicePrivate)); - signals[NAME_LOST] = g_signal_new (INDICATOR_DATETIME_SERVICE_SIGNAL_NAME_LOST, - G_TYPE_FROM_CLASS(klass), - G_SIGNAL_RUN_LAST, - G_STRUCT_OFFSET (IndicatorDatetimeServiceClass, name_lost), - NULL, NULL, - g_cclosure_marshal_VOID__VOID, - G_TYPE_NONE, 0); + signals[SIGNAL_NAME_LOST] = g_signal_new ( + INDICATOR_DATETIME_SERVICE_SIGNAL_NAME_LOST, + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (IndicatorDatetimeServiceClass, name_lost), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); properties[PROP_0] = NULL; |