aboutsummaryrefslogtreecommitdiff
path: root/src/formatter.cpp
diff options
context:
space:
mode:
authorCharles Kerr <charles.kerr@canonical.com>2013-12-17 22:10:18 -0600
committerCharles Kerr <charles.kerr@canonical.com>2013-12-17 22:10:18 -0600
commit3b8833efe6ab21387b6f73b4a4ef757445801623 (patch)
treeb518c7210850d1f2af1b88f52e391a6c6121381a /src/formatter.cpp
parent81c3d4ca5ee8f43e3996bec3be02c566a5e33a4c (diff)
downloadayatana-indicator-datetime-3b8833efe6ab21387b6f73b4a4ef757445801623.tar.gz
ayatana-indicator-datetime-3b8833efe6ab21387b6f73b4a4ef757445801623.tar.bz2
ayatana-indicator-datetime-3b8833efe6ab21387b6f73b4a4ef757445801623.zip
add geoclue, glib test fixtures
Diffstat (limited to 'src/formatter.cpp')
-rw-r--r--src/formatter.cpp462
1 files changed, 462 insertions, 0 deletions
diff --git a/src/formatter.cpp b/src/formatter.cpp
new file mode 100644
index 0000000..4e2f582
--- /dev/null
+++ b/src/formatter.cpp
@@ -0,0 +1,462 @@
+/*
+ * Copyright 2013 Canonical Ltd.
+ *
+ * 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
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranties of
+ * MERCHANTABILITY, SATISFACTORY QUALITY, 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Charles Kerr <charles.kerr@canonical.com>
+ */
+
+#include <datetime/formatter.h>
+
+#include <datetime/clock.h>
+
+#include <glib.h>
+#include <glib/gi18n.h>
+
+#include <locale.h> // setlocale()
+#include <langinfo.h> // nl_langinfo()
+#include <string.h> // strstr()
+
+namespace
+{
+void clearTimer (guint& tag)
+{
+ if (tag)
+ {
+ g_source_remove (tag);
+ tag = 0;
+ }
+}
+
+guint calculate_milliseconds_until_next_minute (GDateTime * now)
+{
+ GDateTime * next;
+ GDateTime * start_of_next;
+ GTimeSpan interval_usec;
+ guint interval_msec;
+
+ 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.1);
+
+ 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);
+ return interval_msec;
+}
+
+gint calculate_milliseconds_until_next_second (GDateTime * now)
+{
+ gint interval_usec;
+ guint interval_msec;
+
+ interval_usec = G_USEC_PER_SEC - g_date_time_get_microsecond (now);
+ interval_msec = (interval_usec + 999) / 1000;
+ return interval_msec;
+}
+
+/*
+ * We 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 either zone's day changes.
+ *
+ * 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.
+ */
+guint calculate_seconds_until_next_fifteen_minutes (GDateTime * now)
+{
+ char * str;
+ gint minute;
+ guint seconds;
+ GTimeSpan diff;
+ GDateTime * next;
+ GDateTime * start_of_next;
+
+ 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),
+ 0.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);
+
+ return seconds;
+}
+} // anonymous namespace
+
+
+
+namespace unity {
+namespace indicator {
+namespace datetime {
+
+class Formatter::Impl
+{
+public:
+
+ Impl (Formatter* owner, const std::shared_ptr<Clock>& clock):
+ owner_(owner),
+ clock_(clock)
+ {
+ owner_->headerFormat.changed().connect([this](const std::string& fmt G_GNUC_UNUSED){updateHeader();});
+ updateHeader();
+
+ restartRelativeTimer();
+ }
+
+ ~Impl()
+ {
+ clearTimer(header_timer_);
+ }
+
+private:
+
+ void updateHeader()
+ {
+ GDateTime * now = clock_->localtime();
+ const time_t unix = g_date_time_to_unix (now);
+ struct tm tm;
+ localtime_r (&unix, &tm);
+ char str[512];
+ strftime (str, sizeof(str), owner_->headerFormat.get().c_str(), &tm);
+ owner_->header.set (str);
+ g_date_time_unref (now);
+
+ restartHeaderTimer();
+ }
+
+ void restartHeaderTimer()
+ {
+ clearTimer(header_timer_);
+
+ const std::string fmt = owner_->headerFormat.get();
+ const bool header_shows_seconds = (fmt.find("%s") != std::string::npos)
+ || (fmt.find("%S") != std::string::npos)
+ || (fmt.find("%T") != std::string::npos)
+ || (fmt.find("%X") != std::string::npos)
+ || (fmt.find("%c") != std::string::npos);
+
+ guint interval_msec;
+ GDateTime * now = clock_->localtime();
+ if (header_shows_seconds)
+ interval_msec = calculate_milliseconds_until_next_second (now);
+ else
+ interval_msec = calculate_milliseconds_until_next_minute (now);
+ g_date_time_unref (now);
+
+ interval_msec += 50; // add a small margin to ensure the callback
+ // fires /after/ next is reached
+
+ header_timer_ = g_timeout_add_full(G_PRIORITY_HIGH, interval_msec, onHeaderTimer, this, nullptr);
+ }
+
+ static gboolean onHeaderTimer(gpointer gself)
+ {
+ static_cast<Formatter::Impl*>(gself)->updateHeader();
+ return G_SOURCE_REMOVE;
+ }
+
+private:
+
+ void restartRelativeTimer()
+ {
+ clearTimer(relative_timer_);
+
+ GDateTime * now = clock_->localtime();
+ const guint seconds = calculate_seconds_until_next_fifteen_minutes(now);
+ relative_timer_ = g_timeout_add_seconds(seconds, onRelativeTimer, this);
+ g_date_time_unref(now);
+ }
+
+ static gboolean onRelativeTimer(gpointer gself)
+ {
+ auto self = static_cast<Formatter::Impl*>(gself);
+ self->owner_->relativeFormatChanged();
+ self->restartRelativeTimer();
+ return G_SOURCE_REMOVE;
+ }
+
+private:
+ Formatter * const owner_;
+ guint header_timer_ = 0;
+ guint relative_timer_ = 0;
+
+public:
+ std::shared_ptr<Clock> clock_;
+};
+
+/***
+****
+***/
+
+Formatter::Formatter(const std::shared_ptr<Clock>& clock):
+ p (new Formatter::Impl(this, clock))
+{
+}
+
+Formatter::~Formatter()
+{
+}
+
+bool
+Formatter::is_locale_12h()
+{
+ static const char *formats_24h[] = {"%H", "%R", "%T", "%OH", "%k", nullptr};
+ const char *t_fmt = nl_langinfo (T_FMT);
+ int i;
+
+ for (i=0; formats_24h[i]; ++i)
+ if (strstr (t_fmt, formats_24h[i]))
+ return false;
+
+ return true;
+}
+
+const char *
+Formatter::T_(const char *msg)
+{
+ /* General strategy here is to make sure LANGUAGE is empty (since that
+ trumps all LC_* vars) and then to temporarily swap LC_TIME and
+ LC_MESSAGES. Then have gettext translate msg.
+
+ We strdup the strings because the setlocale & *env functions do not
+ guarantee anything about the storage used for the string, and thus
+ the string may not be portably safe after multiple calls.
+
+ Note that while you might think g_dcgettext would do the trick here,
+ that actually looks in /usr/share/locale/XX/LC_TIME, not the
+ LC_MESSAGES directory, so we won't find any translation there.
+ */
+
+ char *message_locale = g_strdup(setlocale(LC_MESSAGES, nullptr));
+ const char *time_locale = setlocale (LC_TIME, nullptr);
+ char *language = g_strdup(g_getenv("LANGUAGE"));
+ const char *rv;
+
+ if (language)
+ g_unsetenv("LANGUAGE");
+ setlocale(LC_MESSAGES, time_locale);
+
+ /* Get the LC_TIME version */
+ rv = _(msg);
+
+ /* Put everything back the way it was */
+ setlocale(LC_MESSAGES, message_locale);
+ if (language)
+ g_setenv("LANGUAGE", language, TRUE);
+
+ g_free(message_locale);
+ g_free(language);
+ return rv;
+}
+
+const char *
+Formatter::getDefaultHeaderTimeFormat(bool twelvehour, bool show_seconds)
+{
+ const char * fmt;
+
+ if (twelvehour && show_seconds)
+ /* TRANSLATORS: a strftime(3) format for 12hr time w/seconds */
+ fmt = T_("%l:%M:%S %p");
+ else if (twelvehour)
+ /* TRANSLATORS: a strftime(3) format for 12hr time */
+ fmt = T_("%l:%M %p");
+ else if (show_seconds)
+ /* TRANSLATORS: a strftime(3) format for 24hr time w/seconds */
+ fmt = T_("%H:%M:%S");
+ else
+ /* TRANSLATORS: a strftime(3) format for 24hr time */
+ fmt = T_("%H:%M");
+
+ return fmt;
+}
+
+/***
+****
+***/
+
+namespace
+{
+typedef enum
+{
+ DATE_PROXIMITY_TODAY,
+ DATE_PROXIMITY_TOMORROW,
+ DATE_PROXIMITY_WEEK,
+ DATE_PROXIMITY_FAR
+}
+date_proximity_t;
+
+date_proximity_t getDateProximity (GDateTime * now, GDateTime * time)
+{
+ date_proximity_t prox = DATE_PROXIMITY_FAR;
+ gint now_year, now_month, now_day;
+ gint time_year, time_month, time_day;
+
+ // does it happen today?
+ g_date_time_get_ymd (now, &now_year, &now_month, &now_day);
+ g_date_time_get_ymd (time, &time_year, &time_month, &time_day);
+ if ((now_year == time_year) && (now_month == time_month) && (now_day == time_day))
+ prox = DATE_PROXIMITY_TODAY;
+
+ // does it happen tomorrow?
+ if (prox == DATE_PROXIMITY_FAR)
+ {
+ GDateTime * tomorrow;
+ gint tom_year, tom_month, tom_day;
+
+ tomorrow = g_date_time_add_days (now, 1);
+ g_date_time_get_ymd (tomorrow, &tom_year, &tom_month, &tom_day);
+ if ((tom_year == time_year) && (tom_month == time_month) && (tom_day == time_day))
+ prox = DATE_PROXIMITY_TOMORROW;
+
+ g_date_time_unref (tomorrow);
+ }
+
+ // does it happen this week?
+ if (prox == DATE_PROXIMITY_FAR)
+ {
+ GDateTime * week;
+ GDateTime * week_bound;
+
+ week = g_date_time_add_days (now, 6);
+ week_bound = g_date_time_new_local (g_date_time_get_year(week),
+ g_date_time_get_month (week),
+ g_date_time_get_day_of_month(week),
+ 23, 59, 59.9);
+
+ if (g_date_time_compare (time, week_bound) <= 0)
+ prox = DATE_PROXIMITY_WEEK;
+
+ g_date_time_unref (week_bound);
+ g_date_time_unref (week);
+ }
+
+ return prox;
+}
+} // anonymous namespace
+
+/**
+ * _ a time today should be shown as just the time (e.g. “3:55 PM”)
+ * _ a full-day event today should be shown as “Today”
+ * _ a time any other day this week should be shown as the short version of the
+ * day and time (e.g. “Wed 3:55 PM”)
+ * _ a full-day event tomorrow should be shown as “Tomorrow”
+ * _ a full-day event another day this week should be shown as the
+ * weekday (e.g. “Friday”)
+ * _ a time after this week should be shown as the short version of the day,
+ * date, and time (e.g. “Wed 21 Apr 3:55 PM”)
+ * _ a full-day event after this week should be shown as the short version of
+ * the day and date (e.g. “Wed 21 Apr”).
+ * _ in addition, when presenting the times of upcoming events, the time should
+ * be followed by the timezone if it is different from the one the computer
+ * is currently set to. For example, “Wed 3:55 PM UTC−5”.
+ */
+std::string
+Formatter::getRelativeFormat(GDateTime* then, GDateTime* then_end) const
+{
+ std::string ret;
+ GDateTime * now = p->clock_->localtime();
+
+ if (then != nullptr)
+ {
+ const bool full_day = then_end && (g_date_time_difference (then_end, then) >= G_TIME_SPAN_DAY);
+ const auto prox = getDateProximity (now, then);
+
+ if (full_day)
+ {
+ switch (prox)
+ {
+ case DATE_PROXIMITY_TODAY: ret = _("Today"); break;
+ case DATE_PROXIMITY_TOMORROW: ret = _("Tomorrow"); break;
+ case DATE_PROXIMITY_WEEK: ret = T_("%A"); break;
+ case DATE_PROXIMITY_FAR: ret = T_("%a %d %b"); break;
+ }
+ }
+ else if (is_locale_12h())
+ {
+ switch (prox)
+ {
+ case DATE_PROXIMITY_TODAY: ret = T_("%l:%M %p"); break;
+ case DATE_PROXIMITY_TOMORROW: ret = T_("Tomorrow\u2003%l:%M %p"); break;
+ case DATE_PROXIMITY_WEEK: ret = T_("%a\u2003%l:%M %p"); break;
+ case DATE_PROXIMITY_FAR: ret = T_("%a %d %b\u2003%l:%M %p"); break;
+ }
+ }
+ else
+ {
+ switch (prox)
+ {
+ case DATE_PROXIMITY_TODAY: ret = T_("%H:%M"); break;
+ case DATE_PROXIMITY_TOMORROW: ret = T_("Tomorrow\u2003%H:%M"); break;
+ case DATE_PROXIMITY_WEEK: ret = T_("%a\u2003%H:%M"); break;
+ case DATE_PROXIMITY_FAR: ret = T_("%a %d %b\u2003%H:%M"); break;
+ }
+ }
+
+ /* if it's an appointment in a different timezone (and doesn't run for a full day)
+ then the time should be followed by its timezone. */
+ if ((then_end != nullptr) &&
+ (!full_day) &&
+ ((g_date_time_get_utc_offset (now) != g_date_time_get_utc_offset (then))))
+ {
+ ret += ' ';
+ ret += g_date_time_get_timezone_abbreviation (then);
+ }
+ }
+
+ g_date_time_unref (now);
+ return ret;
+}
+
+/***
+****
+***/
+
+} // namespace datetime
+
+} // namespace indicator
+
+} // namespace unity