diff options
author | Charles Kerr <charles.kerr@canonical.com> | 2013-12-17 22:02:06 -0600 |
---|---|---|
committer | Charles Kerr <charles.kerr@canonical.com> | 2013-12-17 22:02:06 -0600 |
commit | 172246c997d7b20d7e98fc25a7b23f4a79a0f6a6 (patch) | |
tree | a68731b9ef80991661993f1f9fbf4fc189648e20 | |
parent | a1cb4d7802e6e024c842f2a4fa41b91b67162698 (diff) | |
download | ayatana-indicator-datetime-172246c997d7b20d7e98fc25a7b23f4a79a0f6a6.tar.gz ayatana-indicator-datetime-172246c997d7b20d7e98fc25a7b23f4a79a0f6a6.tar.bz2 ayatana-indicator-datetime-172246c997d7b20d7e98fc25a7b23f4a79a0f6a6.zip |
add formatter + tests
-rw-r--r-- | include/datetime/formatter.h | 118 | ||||
-rw-r--r-- | src/formatter-desktop.cpp | 210 | ||||
-rw-r--r-- | tests/test-formatter.cc | 270 |
3 files changed, 598 insertions, 0 deletions
diff --git a/include/datetime/formatter.h b/include/datetime/formatter.h new file mode 100644 index 0000000..66dc212 --- /dev/null +++ b/include/datetime/formatter.h @@ -0,0 +1,118 @@ +/* + * 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> + */ + +#ifndef INDICATOR_DATETIME_FORMATTER_H +#define INDICATOR_DATETIME_FORMATTER_H + +#include <core/property.h> +#include <core/signal.h> + +#include <glib.h> + +namespace unity { +namespace indicator { +namespace datetime { + +class Clock; +class DateTime; + +/*** +**** +***/ + +/** + * \brief Provides the right time format strings based on the profile and user's settings + */ +class Formatter +{ +public: + + /** \brief The time format string for the menu header */ + core::Property<std::string> headerFormat; + + /** \brief The time string for the menu header. (eg, the headerFormat + the clock's time */ + core::Property<std::string> header; + + /** \brief Signal to denote when the relativeFormat has changed. + When this is emitted, clients will want to rebuild their + menuitems that contain relative time strings */ + core::Signal<> relativeFormatChanged; + + /** \brief Generate a relative time format for some time (or time range) + from the current clock's value. For example, a full-day interval + starting at the end of the current clock's day yields "Tomorrow" */ + std::string getRelativeFormat(GDateTime* then, GDateTime* then_end=nullptr) const; + +protected: + + Formatter(const std::shared_ptr<Clock>&); + virtual ~Formatter(); + + /** \brief Returns true if the current locale prefers 12h display instead of 24h */ + static bool is_locale_12h(); + + static const char * getDefaultHeaderTimeFormat(bool twelvehour, bool show_seconds); + + /** \brief Translate the string based on LC_TIME instead of LC_MESSAGES. + The intent of this is to let users set LC_TIME to override + their other locale settings when generating time format string */ + static const char * T_(const char * fmt); + +private: + + Formatter(const Formatter&) =delete; + Formatter& operator=(const Formatter&) =delete; + + class Impl; + std::unique_ptr<Impl> p; +}; + + +/** + * \brief A Formatter for the Desktop and DesktopGreeter profiles. + */ +class DesktopFormatter: public Formatter +{ +public: + DesktopFormatter(const std::shared_ptr<Clock>&); + ~DesktopFormatter(); + +private: + class Impl; + friend Impl; + std::unique_ptr<Impl> p; +}; + + +/** + * \brief A Formatter for Phone and PhoneGreeter profiles. + */ +class PhoneFormatter: public Formatter +{ +public: + PhoneFormatter(const std::shared_ptr<Clock>& clock): Formatter(clock) { + headerFormat.set(getDefaultHeaderTimeFormat(is_locale_12h(), false)); + } +}; + +} // namespace datetime +} // namespace indicator +} // namespace unity + +#endif // INDICATOR_DATETIME_CLOCK_H diff --git a/src/formatter-desktop.cpp b/src/formatter-desktop.cpp new file mode 100644 index 0000000..8390967 --- /dev/null +++ b/src/formatter-desktop.cpp @@ -0,0 +1,210 @@ +/* + * 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/settings-shared.h> + +#include <glib.h> +#include <gio/gio.h> + +namespace unity { +namespace indicator { +namespace datetime { + +/*** +**** +***/ + +class DesktopFormatter::Impl +{ +public: + + Impl (DesktopFormatter * owner, const std::shared_ptr<Clock>& clock): + owner_(owner), + clock_(clock), + settings_(g_settings_new(SETTINGS_INTERFACE)) + { + const gchar * const keys[] = { "changed::" SETTINGS_SHOW_SECONDS_S, + "changed::" SETTINGS_TIME_FORMAT_S, + "changed::" SETTINGS_TIME_FORMAT_S, + "changed::" SETTINGS_CUSTOM_TIME_FORMAT_S, + "changed::" SETTINGS_SHOW_DAY_S, + "changed::" SETTINGS_SHOW_DATE_S, + "changed::" SETTINGS_SHOW_YEAR_S }; + for (guint i=0, n=G_N_ELEMENTS(keys); i<n; i++) + g_signal_connect(settings_, keys[i], G_CALLBACK(onSettingsChanged), this); + + rebuildHeaderFormat(); + } + + ~Impl() + { + g_signal_handlers_disconnect_by_data (settings_, this); + g_object_unref (settings_); + } + +private: + + static void onSettingsChanged (GSettings * changed G_GNUC_UNUSED, + const gchar * key G_GNUC_UNUSED, + gpointer gself) + { + static_cast<Impl*>(gself)->rebuildHeaderFormat(); + } + + void rebuildHeaderFormat() + { + gchar * fmt = getHeaderLabelFormatString (settings_); + owner_->headerFormat.set(fmt); + g_free (fmt); + } + +private: + + gchar* getHeaderLabelFormatString (GSettings * s) const + { + char * fmt; + const TimeFormatMode mode = (TimeFormatMode) g_settings_get_enum (s, SETTINGS_TIME_FORMAT_S); + + if (mode == TIME_FORMAT_MODE_CUSTOM) + { + fmt = g_settings_get_string (s, SETTINGS_CUSTOM_TIME_FORMAT_S); + } + else + { + const bool show_day = g_settings_get_boolean (s, SETTINGS_SHOW_DAY_S); + const bool show_date = g_settings_get_boolean (s, SETTINGS_SHOW_DATE_S); + const bool show_year = show_date && g_settings_get_boolean (s, SETTINGS_SHOW_YEAR_S); + const char * date_fmt = getDateFormat (show_day, show_date, show_year); + const char * time_fmt = getFullTimeFormatString (s); + fmt = joinDateAndTimeFormatStrings (date_fmt, time_fmt); + } + + return fmt; + } + + const gchar* T_(const gchar* in) const + { + return owner_->T_(in); + } + + const gchar* getDateFormat (bool show_day, bool show_date, bool show_year) const + { + const char * fmt; + + if (show_day && show_date && show_year) + /* TRANSLATORS: a strftime(3) format showing the weekday, date, and year */ + fmt = T_("%a %b %e %Y"); + else if (show_day && show_date) + fmt = T_("%a %b %e"); + else if (show_day && show_year) + /* TRANSLATORS: a strftime(3) format showing the weekday and year. */ + fmt = T_("%a %Y"); + else if (show_day) + /* TRANSLATORS: a strftime(3) format showing the weekday. */ + fmt = T_("%a"); + else if (show_date && show_year) + /* TRANSLATORS: a strftime(3) format showing the date and year */ + fmt = T_("%b %e %Y"); + else if (show_date) + /* TRANSLATORS: a strftime(3) format showing the date */ + fmt = T_("%b %e"); + else if (show_year) + /* TRANSLATORS: a strftime(3) format showing the year */ + fmt = T_("%Y"); + else + fmt = nullptr; + + return fmt; + } + + const gchar * getFullTimeFormatString (GSettings * settings) const + { + const bool show_seconds = g_settings_get_boolean (settings, SETTINGS_SHOW_SECONDS_S); + + bool twelvehour; + switch (g_settings_get_enum (settings, SETTINGS_TIME_FORMAT_S)) + { + case TIME_FORMAT_MODE_LOCALE_DEFAULT: + twelvehour = is_locale_12h(); + break; + + case TIME_FORMAT_MODE_24_HOUR: + twelvehour = FALSE; + break; + + default: + twelvehour = TRUE; + break; + } + + return owner_->getDefaultHeaderTimeFormat (twelvehour, show_seconds); + } + + gchar* joinDateAndTimeFormatStrings (const char * date_string, const char * time_string) const + { + gchar * str; + + if (date_string && time_string) + { + /* TRANSLATORS: This is a format string passed to strftime to combine the + * date and the time. The value of "%s\u2003%s" will result in a + * string like this in US English 12-hour time: 'Fri Jul 16 11:50 AM'. + * The space in between date and time is a Unicode en space + * (E28082 in UTF-8 hex). */ + str = g_strdup_printf ("%s\u2003%s", date_string, time_string); + } + else if (date_string) + { + str = g_strdup (date_string); + } + else // time_string + { + str = g_strdup (time_string); + } + + return str; + } + +private: + + DesktopFormatter * const owner_; + std::shared_ptr<Clock> clock_; + GSettings * settings_; +}; + +/*** +**** +***/ + +DesktopFormatter::DesktopFormatter (const std::shared_ptr<Clock>& clock): + Formatter(clock), + p(new Impl(this, clock)) +{ +} + +DesktopFormatter::~DesktopFormatter() = default; + +/*** +**** +***/ + +} // namespace datetime +} // namespace indicator +} // namespace unity diff --git a/tests/test-formatter.cc b/tests/test-formatter.cc new file mode 100644 index 0000000..641338b --- /dev/null +++ b/tests/test-formatter.cc @@ -0,0 +1,270 @@ + +/* + * Copyright 2013 Canonical Ltd. + * + * Authors: + * Charles Kerr <charles.kerr@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 + * 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/>. + */ + +#include "clock-mock.h" +#include "glib-fixture.h" + +#include <datetime/formatter.h> +#include <datetime/settings-shared.h> + +#include <glib/gi18n.h> + +#include <langinfo.h> +#include <locale.h> + +using unity::indicator::datetime::Clock; +using unity::indicator::datetime::MockClock; +using unity::indicator::datetime::PhoneFormatter; +using unity::indicator::datetime::DesktopFormatter; + +/*** +**** +***/ + +class FormatterFixture: public GlibFixture +{ + private: + + typedef GlibFixture super; + gchar * original_locale = nullptr; + + protected: + + GSettings * settings = nullptr; + + virtual void SetUp () + { + super::SetUp (); + + settings = g_settings_new (SETTINGS_INTERFACE); + + original_locale = g_strdup (setlocale (LC_TIME, NULL)); + } + + virtual void TearDown () + { + g_clear_object (&settings); + + setlocale (LC_TIME, original_locale); + g_clear_pointer (&original_locale, g_free); + + super::TearDown (); + } + + bool SetLocale (const char * expected_locale, const char * name) + { + setlocale (LC_TIME, expected_locale); + const char * actual_locale = setlocale (LC_TIME, NULL); + if (!g_strcmp0 (expected_locale, actual_locale)) + { + return true; + } + else + { + g_warning ("Unable to set locale to %s; skipping %s locale tests.", expected_locale, name); + return false; + } + } + + inline bool Set24hLocale () { return SetLocale ("C", "24h"); } + inline bool Set12hLocale () { return SetLocale ("en_US.utf8", "12h"); } +}; + + +/** + * Test the phone header format + */ +TEST_F (FormatterFixture, TestPhoneHeader) +{ + GDateTime * now = g_date_time_new_local (2020, 10, 31, 18, 30, 59); + std::shared_ptr<MockClock> mock (new MockClock(now)); + std::shared_ptr<Clock> clock = std::dynamic_pointer_cast<Clock>(mock); + + // test the default value in a 24h locale + if (Set24hLocale ()) + { + PhoneFormatter formatter (clock); + EXPECT_EQ (std::string("%H:%M"), formatter.headerFormat.get()); + EXPECT_EQ (std::string("18:30"), formatter.header.get()); + } + + // test the default value in a 12h locale + if (Set12hLocale ()) + { + PhoneFormatter formatter (clock); + EXPECT_EQ (std::string("%l:%M %p"), formatter.headerFormat.get()); + EXPECT_EQ (std::string(" 6:30 PM"), formatter.header.get()); + } +} + +#define EM_SPACE "\u2003" + +/** + * Test the default values of the desktop header format + */ +TEST_F (FormatterFixture, TestDesktopHeader) +{ + struct { + bool is_12h; + bool show_day; + bool show_date; + bool show_year; + const char * expected_format_string; + } test_cases[] = { + { false, false, false, false, "%H:%M" }, + { false, false, false, true, "%H:%M" }, // show_year is ignored iff show_date is false + { false, false, true, false, "%b %e" EM_SPACE "%H:%M" }, + { false, false, true, true, "%b %e %Y" EM_SPACE "%H:%M" }, + { false, true, false, false, "%a" EM_SPACE "%H:%M" }, + { false, true, false, true, "%a" EM_SPACE "%H:%M" }, // show_year is ignored iff show_date is false + { false, true, true, false, "%a %b %e" EM_SPACE "%H:%M" }, + { false, true, true, true, "%a %b %e %Y" EM_SPACE "%H:%M" }, + { true, false, false, false, "%l:%M %p" }, + { true, false, false, true, "%l:%M %p" }, // show_year is ignored iff show_date is false + { true, false, true, false, "%b %e" EM_SPACE "%l:%M %p" }, + { true, false, true, true, "%b %e %Y" EM_SPACE "%l:%M %p" }, + { true, true, false, false, "%a" EM_SPACE "%l:%M %p" }, + { true, true, false, true, "%a" EM_SPACE "%l:%M %p" }, // show_year is ignored iff show_date is false + { true, true, true, false, "%a %b %e" EM_SPACE "%l:%M %p" }, + { true, true, true, true, "%a %b %e %Y" EM_SPACE "%l:%M %p" } + }; + + GDateTime * now = g_date_time_new_local (2020, 10, 31, 18, 30, 59); + std::shared_ptr<MockClock> mock (new MockClock(now)); + std::shared_ptr<Clock> clock = std::dynamic_pointer_cast<Clock>(mock); + + for (int i=0, n=G_N_ELEMENTS(test_cases); i<n; i++) + { + if (test_cases[i].is_12h ? Set12hLocale() : Set24hLocale()) + { + DesktopFormatter f (clock); + + g_settings_set_boolean (settings, SETTINGS_SHOW_DAY_S, test_cases[i].show_day); + g_settings_set_boolean (settings, SETTINGS_SHOW_DATE_S, test_cases[i].show_date); + g_settings_set_boolean (settings, SETTINGS_SHOW_YEAR_S, test_cases[i].show_year); + + ASSERT_STREQ (test_cases[i].expected_format_string, f.headerFormat.get().c_str()); + + g_settings_reset (settings, SETTINGS_SHOW_DAY_S); + g_settings_reset (settings, SETTINGS_SHOW_DATE_S); + g_settings_reset (settings, SETTINGS_SHOW_YEAR_S); + } + } +} + +/** + * Test the default values of the desktop header format + */ +TEST_F (FormatterFixture, TestUpcomingTimes) +{ + GDateTime * a = g_date_time_new_local (2020, 10, 31, 18, 30, 59); + + struct { + gboolean is_12h; + GDateTime * now; + GDateTime * then; + GDateTime * then_end; + const char * expected_format_string; + } test_cases[] = { + { true, g_date_time_ref(a), g_date_time_ref(a), NULL, "%l:%M %p" }, // identical time + { true, g_date_time_ref(a), g_date_time_add_hours(a,1), NULL, "%l:%M %p" }, // later today + { true, g_date_time_ref(a), g_date_time_add_days(a,1), NULL, "Tomorrow" EM_SPACE "%l:%M %p" }, // tomorrow + { true, g_date_time_ref(a), g_date_time_add_days(a,2), NULL, "%a" EM_SPACE "%l:%M %p" }, + { true, g_date_time_ref(a), g_date_time_add_days(a,6), NULL, "%a" EM_SPACE "%l:%M %p" }, + { true, g_date_time_ref(a), g_date_time_add_days(a,7), NULL, "%a %d %b" EM_SPACE "%l:%M %p" }, // over one week away + + { false, g_date_time_ref(a), g_date_time_ref(a), NULL, "%H:%M" }, // identical time + { false, g_date_time_ref(a), g_date_time_add_hours(a,1), NULL, "%H:%M" }, // later today + { false, g_date_time_ref(a), g_date_time_add_days(a,1), NULL, "Tomorrow" EM_SPACE "%H:%M" }, // tomorrow + { false, g_date_time_ref(a), g_date_time_add_days(a,2), NULL, "%a" EM_SPACE "%H:%M" }, + { false, g_date_time_ref(a), g_date_time_add_days(a,6), NULL, "%a" EM_SPACE "%H:%M" }, + { false, g_date_time_ref(a), g_date_time_add_days(a,7), NULL, "%a %d %b" EM_SPACE "%H:%M" } // over one week away + }; + + for (int i=0, n=G_N_ELEMENTS(test_cases); i<n; i++) + { + if (test_cases[i].is_12h ? Set12hLocale() : Set24hLocale()) + { + std::shared_ptr<MockClock> mock (new MockClock(test_cases[i].now)); + std::shared_ptr<Clock> clock = std::dynamic_pointer_cast<Clock>(mock); + DesktopFormatter f (clock); + + std::string fmt = f.getRelativeFormat (test_cases[i].then, test_cases[i].then_end); + ASSERT_STREQ (test_cases[i].expected_format_string, fmt.c_str()); + + g_clear_pointer (&test_cases[i].now, g_date_time_unref); + g_clear_pointer (&test_cases[i].then, g_date_time_unref); + g_clear_pointer (&test_cases[i].then_end, g_date_time_unref); + } + } + + g_date_time_unref (a); +} + + +/** + * Test the default values of the desktop header format + */ +TEST_F (FormatterFixture, TestEventTimes) +{ + GDateTime * day = g_date_time_new_local (2013, 1, 1, 13, 0, 0); + GDateTime * day_begin = g_date_time_new_local (2013, 1, 1, 13, 0, 0); + GDateTime * day_end = g_date_time_add_days (day_begin, 1); + GDateTime * tomorrow_begin = g_date_time_add_days (day_begin, 1); + GDateTime * tomorrow_end = g_date_time_add_days (tomorrow_begin, 1); + + struct { + bool is_12h; + GDateTime * now; + GDateTime * then; + GDateTime * then_end; + const char * expected_format_string; + } test_cases[] = { + { false, g_date_time_ref(day), g_date_time_ref(day_begin), g_date_time_ref(day_end), _("Today") }, + { true, g_date_time_ref(day), g_date_time_ref(day_begin), g_date_time_ref(day_end), _("Today") }, + { false, g_date_time_ref(day), g_date_time_ref(tomorrow_begin), g_date_time_ref(tomorrow_end), _("Tomorrow") }, + { true, g_date_time_ref(day), g_date_time_ref(tomorrow_begin), g_date_time_ref(tomorrow_end), _("Tomorrow") } + }; + + for (int i=0, n=G_N_ELEMENTS(test_cases); i<n; i++) + { + if (test_cases[i].is_12h ? Set12hLocale() : Set24hLocale()) + { + std::shared_ptr<MockClock> mock (new MockClock(test_cases[i].now)); + std::shared_ptr<Clock> clock = std::dynamic_pointer_cast<Clock>(mock); + DesktopFormatter f (clock); + + std::string fmt = f.getRelativeFormat (test_cases[i].then, test_cases[i].then_end); + ASSERT_STREQ (test_cases[i].expected_format_string, fmt.c_str()); + + g_clear_pointer (&test_cases[i].now, g_date_time_unref); + g_clear_pointer (&test_cases[i].then, g_date_time_unref); + g_clear_pointer (&test_cases[i].then_end, g_date_time_unref); + } + } + + g_date_time_unref (tomorrow_end); + g_date_time_unref (tomorrow_begin); + g_date_time_unref (day_end); + g_date_time_unref (day_begin); + g_date_time_unref (day); +} + + |