diff options
50 files changed, 2167 insertions, 411 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index fcee8d5..9b468a5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,10 +38,10 @@ pkg_check_modules (SERVICE_DEPS REQUIRED libical>=0.48 libecal-1.2>=3.5 libedataserver-1.2>=3.5 + libcanberra>=0.12 libnotify>=0.7.6 url-dispatcher-1>=1 - properties-cpp>=0.0.1 - json-glib-1.0>=0.16.2) + properties-cpp>=0.0.1) include_directories (SYSTEM ${SERVICE_DEPS_INCLUDE_DIRS}) ## diff --git a/MERGE-REVIEW b/MERGE-REVIEW new file mode 100644 index 0000000..5e40f45 --- /dev/null +++ b/MERGE-REVIEW @@ -0,0 +1,19 @@ + +This documents the expections that the project has on what both submitters +and reviewers should ensure that they've done for a merge into the project. + +== Submitter Responsibilities == + + * Ensure the project compiles and the test suite executes without error + * Ensure that non-obvious code has comments explaining it + * If the change works on specific profiles, please include those in the merge description. + +== Reviewer Responsibilities == + + * Did the Jenkins build compile? Pass? Run unit tests successfully? + * Are there appropriate tests to cover any new functionality? + * If the description says this effects the phone profile: + * Run tests indicator-datetime/unity8* + * If the description says this effects the desktop profile: + * Run tests indicator-datetime/unity7* + diff --git a/data/com.canonical.indicator.datetime b/data/com.canonical.indicator.datetime index 7fa1e34..b9de626 100644 --- a/data/com.canonical.indicator.datetime +++ b/data/com.canonical.indicator.datetime @@ -9,6 +9,9 @@ ObjectPath=/com/canonical/indicator/datetime/desktop [desktop_greeter] ObjectPath=/com/canonical/indicator/datetime/desktop_greeter +[desktop_lockscreen] +ObjectPath=/com/canonical/indicator/datetime/desktop_greeter + [phone] ObjectPath=/com/canonical/indicator/datetime/phone diff --git a/debian/changelog b/debian/changelog index 097df7e..09f58b4 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,79 @@ +indicator-datetime (13.10.0+14.04.20140321-0ubuntu1) trusty; urgency=low + + [ Charles Kerr ] + * If we notify_notification_show() fails, we shouldn't play the alarm + sound. The alarm sound loops until the user dismisses it, which will + never happen if we can't talk to unity-notiifications. So the + outcome in this error case is that the alarm plays forever and can't + be dismissed by the user. (LP: #1295237) + * Add debug logging of what capabilities the notification server said + it supports. (LP: #1295271) + + -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Fri, 21 Mar 2014 05:23:04 +0000 + +indicator-datetime (13.10.0+14.04.20140314.1-0ubuntu1) trusty; urgency=low + + [ Charles Kerr ] + * When the user clicks on a date in the calendar, update the "Upcoming + Events" section to show events starting at that date. (LP: #1290169) + * Don't use EDS if we're in the greeter. (LP: #1256130) + * Don't show the "Add Event..." button if the calendar app can't be + found. (LP: #1250632) + + -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Fri, 14 Mar 2014 17:37:32 +0000 + +indicator-datetime (13.10.0+14.04.20140311.1-0ubuntu1) trusty; urgency=low + + [ Lars Uebernickel ] + * add desktop_lockscreen profile + + -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Tue, 11 Mar 2014 18:16:48 +0000 + +indicator-datetime (13.10.0+14.04.20140227.1-0ubuntu1) trusty; urgency=low + + [ Charles Kerr ] + * When the notification engine is notify-osd, use bubble notifications + instead of the phone's snap decisions. (LP: #1283142) + + -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Thu, 27 Feb 2014 18:44:10 +0000 + +indicator-datetime (13.10.0+14.04.20140227-0ubuntu1) trusty; urgency=low + + [ Charles Kerr ] + * Don't log E_CLIENT_ERROR_NOT_SUPPORTED errors returned by + e_cal_client_get_attachment_uris() -- we can silently interpret that + as 'no attachments' (LP: #1285212) + * DateTime::format(), don't pass NULL to a std::string's assignment + operator (LP: #1285243) + * In EdsPlanner's get_appointments(), sort 'em before returning them + to the caller. (LP: #1285249) + + -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Thu, 27 Feb 2014 10:59:26 +0000 + +indicator-datetime (13.10.0+14.04.20140225-0ubuntu1) trusty; urgency=low + + [ Charles Kerr ] + * Test the EClient's color hint string for NULL before passing it to a + std::string constructor. (LP: #1283834) + * Test the EDS component summary for NULL before using it in a + std::string. (LP: #1280341) + * In the alarms menu, don't let iterations of recurring events drown + out everything else. + * Fix g_assert_if_reached() in EdsPlanner::on_source_enabled(). (LP: + #1283610) + + -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Tue, 25 Feb 2014 16:58:43 +0000 + +indicator-datetime (13.10.0+14.04.20140219.1-0ubuntu1) trusty; urgency=low + + [ Charles Kerr ] + * support for ubuntu-clock-app's alarms (LP: #1233176) + + [ Ted Gould ] + * Adding acceptance tests and merge review policies + + -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Wed, 19 Feb 2014 18:02:43 +0000 + indicator-datetime (13.10.0+14.04.20140217-0ubuntu1) trusty; urgency=low [ Robert Ancell ] diff --git a/debian/control b/debian/control index 423f8b8..84f76ca 100644 --- a/debian/control +++ b/debian/control @@ -16,13 +16,13 @@ Build-Depends: cmake, libgtest-dev, libglib2.0-dev (>= 2.35.4), libnotify-dev (>= 0.7.6), + libcanberra-dev, libido3-0.1-dev (>= 0.2.90), libgeoclue-dev (>= 0.12.0), libecal1.2-dev (>= 3.5), libical-dev (>= 1.0), libgtk-3-dev (>= 3.1.4), libcairo2-dev (>= 1.10), - libjson-glib-dev, libpolkit-gobject-1-dev, libedataserver1.2-dev (>= 3.5), libgconf2-dev (>= 2.31), diff --git a/include/datetime/actions-live.h b/include/datetime/actions-live.h index 3607836..a24b844 100644 --- a/include/datetime/actions-live.h +++ b/include/datetime/actions-live.h @@ -42,6 +42,7 @@ public: void open_desktop_settings(); void open_phone_settings(); void open_phone_clock_app(); + bool can_open_planner() const; void open_planner(); void open_planner_at(const DateTime&); void open_appointment(const std::string& uid); diff --git a/include/datetime/actions.h b/include/datetime/actions.h index 99e78f5..2c4217c 100644 --- a/include/datetime/actions.h +++ b/include/datetime/actions.h @@ -45,6 +45,7 @@ public: virtual void open_desktop_settings() =0; virtual void open_phone_settings() =0; virtual void open_phone_clock_app() =0; + virtual bool can_open_planner() const = 0; virtual void open_planner() =0; virtual void open_planner_at(const DateTime&) =0; virtual void open_appointment(const std::string& uid) =0; diff --git a/include/datetime/clock-watcher.h b/include/datetime/clock-watcher.h new file mode 100644 index 0000000..90bbb63 --- /dev/null +++ b/include/datetime/clock-watcher.h @@ -0,0 +1,75 @@ +/* + * Copyright 2014 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_CLOCK_WATCHER_H +#define INDICATOR_DATETIME_CLOCK_WATCHER_H + +#include <datetime/appointment.h> +#include <datetime/clock.h> +#include <datetime/planner-upcoming.h> + +#include <core/signal.h> + +#include <memory> +#include <set> +#include <string> + +namespace unity { +namespace indicator { +namespace datetime { + + +/** + * \brief Watches the clock and appointments to notify when an + * appointment's time is reached. + */ +class ClockWatcher +{ +public: + ClockWatcher() =default; + virtual ~ClockWatcher() =default; + virtual core::Signal<const Appointment&>& alarm_reached() = 0; +}; + + +/** + * \brief A #ClockWatcher implementation + */ +class ClockWatcherImpl: public ClockWatcher +{ +public: + ClockWatcherImpl(const std::shared_ptr<Clock>& clock, + const std::shared_ptr<UpcomingPlanner>& upcoming_planner); + ~ClockWatcherImpl() =default; + core::Signal<const Appointment&>& alarm_reached(); + +private: + void pulse(); + std::set<std::string> m_triggered; + const std::shared_ptr<Clock> m_clock; + const std::shared_ptr<UpcomingPlanner> m_upcoming_planner; + core::Signal<const Appointment&> m_alarm_reached; +}; + + +} // namespace datetime +} // namespace indicator +} // namespace unity + +#endif // INDICATOR_DATETIME_CLOCK_WATCHER_H diff --git a/include/datetime/date-time.h b/include/datetime/date-time.h index 2ad7856..f861c2e 100644 --- a/include/datetime/date-time.h +++ b/include/datetime/date-time.h @@ -36,21 +36,29 @@ class DateTime { public: static DateTime NowLocal(); + static DateTime Local(int years, int months, int days, int hours, int minutes, int seconds); + explicit DateTime(time_t t); explicit DateTime(GDateTime* in=nullptr); DateTime& operator=(GDateTime* in); DateTime& operator=(const DateTime& in); DateTime to_timezone(const std::string& zone) const; + DateTime add_full(int years, int months, int days, int hours, int minutes, double seconds) const; void reset(GDateTime* in=nullptr); GDateTime* get() const; GDateTime* operator()() const {return get();} std::string format(const std::string& fmt) const; + void ymd(int& year, int& month, int& day) const; int day_of_month() const; + int hour() const; + int minute() const; + double seconds() const; int64_t to_unix() const; bool operator<(const DateTime& that) const; + bool operator<=(const DateTime& that) const; bool operator!=(const DateTime& that) const; bool operator==(const DateTime& that) const; diff --git a/include/datetime/engine-eds.h b/include/datetime/engine-eds.h new file mode 100644 index 0000000..4b260a8 --- /dev/null +++ b/include/datetime/engine-eds.h @@ -0,0 +1,75 @@ +/* + * Copyright 2014 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_ENGINE_EDS__H +#define INDICATOR_DATETIME_ENGINE_EDS__H + +#include <datetime/engine.h> + +#include <datetime/appointment.h> +#include <datetime/date-time.h> +#include <datetime/timezone.h> + +#include <functional> +#include <vector> + +namespace unity { +namespace indicator { +namespace datetime { + +/**** +***** +****/ + +/** + * Class wrapper around EDS so multiple #EdsPlanners can share resources + * + * @see EdsPlanner + */ +class EdsEngine: public Engine +{ +public: + EdsEngine(); + ~EdsEngine(); + + void get_appointments(const DateTime& begin, + const DateTime& end, + const Timezone& default_timezone, + std::function<void(const std::vector<Appointment>&)> appointment_func); + + core::Signal<>& changed(); + +private: + class Impl; + std::unique_ptr<Impl> p; + + // we've got a unique_ptr here, disable copying... + EdsEngine(const EdsEngine&) =delete; + EdsEngine& operator=(const EdsEngine&) =delete; +}; + +/*** +**** +***/ + +} // namespace datetime +} // namespace indicator +} // namespace unity + +#endif // INDICATOR_DATETIME_ENGINE_EDS__H diff --git a/include/datetime/engine-mock.h b/include/datetime/engine-mock.h new file mode 100644 index 0000000..ecbf102 --- /dev/null +++ b/include/datetime/engine-mock.h @@ -0,0 +1,68 @@ +/* + * Copyright 2014 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_ENGINE_MOCK__H +#define INDICATOR_DATETIME_ENGINE_MOCK__H + +#include <datetime/engine.h> + +namespace unity { +namespace indicator { +namespace datetime { + +/**** +***** +****/ + +/** + * A no-op #Engine + * + * @see Engine + */ +class MockEngine: public Engine +{ +public: + MockEngine() =default; + ~MockEngine() =default; + + void get_appointments(const DateTime& /*begin*/, + const DateTime& /*end*/, + const Timezone& /*default_timezone*/, + std::function<void(const std::vector<Appointment>&)> appointment_func) { + appointment_func(m_appointments); + } + + core::Signal<>& changed() { + return m_changed; + } + +private: + core::Signal<> m_changed; + std::vector<Appointment> m_appointments; +}; + +/*** +**** +***/ + +} // namespace datetime +} // namespace indicator +} // namespace unity + +#endif // INDICATOR_DATETIME_ENGINE_NOOP__H diff --git a/include/datetime/engine.h b/include/datetime/engine.h new file mode 100644 index 0000000..2e8237e --- /dev/null +++ b/include/datetime/engine.h @@ -0,0 +1,68 @@ +/* + * Copyright 2014 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_ENGINE__H +#define INDICATOR_DATETIME_ENGINE__H + +#include <datetime/appointment.h> +#include <datetime/date-time.h> +#include <datetime/timezone.h> + +#include <functional> +#include <vector> + +namespace unity { +namespace indicator { +namespace datetime { + +/**** +***** +****/ + +/** + * Class wrapper around the backend that generates appointments + * + * @see EdsEngine + * @see EdsPlanner + */ +class Engine +{ +public: + virtual ~Engine() =default; + + virtual void get_appointments(const DateTime& begin, + const DateTime& end, + const Timezone& default_timezone, + std::function<void(const std::vector<Appointment>&)> appointment_func) =0; + + virtual core::Signal<>& changed() =0; + +protected: + Engine() =default; +}; + +/*** +**** +***/ + +} // namespace datetime +} // namespace indicator +} // namespace unity + +#endif // INDICATOR_DATETIME_ENGINE__H diff --git a/include/datetime/planner-month.h b/include/datetime/planner-month.h new file mode 100644 index 0000000..492529f --- /dev/null +++ b/include/datetime/planner-month.h @@ -0,0 +1,56 @@ +/* + * Copyright 2014 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_PLANNER_MONTH_H +#define INDICATOR_DATETIME_PLANNER_MONTH_H + +#include <datetime/planner.h> + +#include <datetime/date-time.h> +#include <datetime/planner-range.h> + +#include <memory> // std::shared_ptr + +namespace unity { +namespace indicator { +namespace datetime { + +/** + * \brief A #Planner that contains appointments for a specified calendar month + */ +class MonthPlanner: public Planner +{ +public: + MonthPlanner(const std::shared_ptr<RangePlanner>& range_planner, + const DateTime& month_in); + ~MonthPlanner() =default; + + core::Property<std::vector<Appointment>>& appointments(); + core::Property<DateTime>& month(); + +private: + std::shared_ptr<RangePlanner> m_range_planner; + core::Property<DateTime> m_month; +}; + +} // namespace datetime +} // namespace indicator +} // namespace unity + +#endif // INDICATOR_DATETIME_PLANNER_MONTH_H diff --git a/include/datetime/planner-range.h b/include/datetime/planner-range.h new file mode 100644 index 0000000..25334a6 --- /dev/null +++ b/include/datetime/planner-range.h @@ -0,0 +1,84 @@ +/* + * Copyright 2014 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_PLANNER_RANGE_H +#define INDICATOR_DATETIME_PLANNER_RANGE_H + +#include <datetime/planner.h> + +#include <datetime/date-time.h> +#include <datetime/engine.h> + +namespace unity { +namespace indicator { +namespace datetime { + +/** + * \brief A #Planner that contains appointments in a specified date range + * + * @see Planner + */ +class RangePlanner: public Planner +{ +public: + virtual ~RangePlanner() =default; + virtual core::Property<std::pair<DateTime,DateTime>>& range() =0; + +protected: + RangePlanner() =default; +}; + +/** + * \brief A #RangePlanner that uses an #Engine to generate appointments + * + * @see Planner + */ +class SimpleRangePlanner: public RangePlanner +{ +public: + SimpleRangePlanner(const std::shared_ptr<Engine>& engine, + const std::shared_ptr<Timezone>& timezone); + virtual ~SimpleRangePlanner(); + + core::Property<std::vector<Appointment>>& appointments(); + core::Property<std::pair<DateTime,DateTime>>& range(); + +private: + // rebuild scaffolding + void rebuild_soon(); + virtual void rebuild_now(); + static gboolean rebuild_now_static(gpointer); + guint m_rebuild_tag = 0; + + std::shared_ptr<Engine> m_engine; + std::shared_ptr<Timezone> m_timezone; + core::Property<std::pair<DateTime,DateTime>> m_range; + core::Property<std::vector<Appointment>> m_appointments; + + // we've got a GSignal tag here, so disable copying + SimpleRangePlanner(const RangePlanner&) =delete; + SimpleRangePlanner& operator=(const RangePlanner&) =delete; +}; + + +} // namespace datetime +} // namespace indicator +} // namespace unity + +#endif // INDICATOR_DATETIME_PLANNER_RANGE_H diff --git a/include/datetime/planner-upcoming.h b/include/datetime/planner-upcoming.h new file mode 100644 index 0000000..683543f --- /dev/null +++ b/include/datetime/planner-upcoming.h @@ -0,0 +1,56 @@ +/* + * Copyright 2014 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_PLANNER_UPCOMING_H +#define INDICATOR_DATETIME_PLANNER_UPCOMING_H + +#include <datetime/planner.h> + +#include <datetime/date-time.h> +#include <datetime/planner-range.h> + +#include <memory> // std::shared_ptr + +namespace unity { +namespace indicator { +namespace datetime { + +/** + * \brief A collection of upcoming appointments starting from the specified date + */ +class UpcomingPlanner: public Planner +{ +public: + UpcomingPlanner(const std::shared_ptr<RangePlanner>& range_planner, + const DateTime& date); + ~UpcomingPlanner() =default; + + core::Property<std::vector<Appointment>>& appointments(); + core::Property<DateTime>& date(); + +private: + std::shared_ptr<RangePlanner> m_range_planner; + core::Property<DateTime> m_date; +}; + +} // namespace datetime +} // namespace indicator +} // namespace unity + +#endif // INDICATOR_DATETIME_PLANNER_UPCOMING_H diff --git a/include/datetime/planner.h b/include/datetime/planner.h index 376a31f..e6ef927 100644 --- a/include/datetime/planner.h +++ b/include/datetime/planner.h @@ -32,41 +32,16 @@ namespace indicator { namespace datetime { /** - * \brief Simple appointment book - * - * @see EdsPlanner - * @see State + * \brief Simple collection of appointments */ class Planner { public: virtual ~Planner() =default; - - /** - * \brief Timestamp used to determine the appointments in the `upcoming' and `this_month' properties. - * Setting this value will cause the planner to re-query its backend and - * update the `upcoming' and `this_month' properties. - */ - core::Property<DateTime> time; - - /** - * \brief The next few appointments that follow the time specified in the time property. - */ - core::Property<std::vector<Appointment>> upcoming; - - /** - * \brief The appointments that occur in the same month as the time property - */ - core::Property<std::vector<Appointment>> this_month; + virtual core::Property<std::vector<Appointment>>& appointments() =0; protected: Planner() =default; - -private: - - // disable copying - Planner(const Planner&) =delete; - Planner& operator=(const Planner&) =delete; }; } // namespace datetime diff --git a/include/datetime/snap.h b/include/datetime/snap.h new file mode 100644 index 0000000..a493772 --- /dev/null +++ b/include/datetime/snap.h @@ -0,0 +1,51 @@ +/* + * Copyright 2014 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_SNAP_H +#define INDICATOR_DATETIME_SNAP_H + +#include <datetime/appointment.h> + +#include <memory> +#include <functional> + +namespace unity { +namespace indicator { +namespace datetime { + +/** + * \brief Pops up Snap Decisions for appointments + */ +class Snap +{ +public: + Snap(); + virtual ~Snap(); + + typedef std::function<void(const Appointment&)> appointment_func; + void operator()(const Appointment& appointment, + appointment_func show, + appointment_func dismiss); +}; + +} // namespace datetime +} // namespace indicator +} // namespace unity + +#endif // INDICATOR_DATETIME_SNAP_H diff --git a/include/datetime/state.h b/include/datetime/state.h index 414be32..0e1043c 100644 --- a/include/datetime/state.h +++ b/include/datetime/state.h @@ -22,7 +22,8 @@ #include <datetime/clock.h> #include <datetime/locations.h> -#include <datetime/planner.h> +#include <datetime/planner-month.h> +#include <datetime/planner-upcoming.h> #include <datetime/settings.h> #include <datetime/timezones.h> @@ -60,9 +61,14 @@ struct State section of the #Menu */ std::shared_ptr<Locations> locations; - /** \brief The appointments to be displayed in the Calendar and - Appointments sections of the #Menu */ - std::shared_ptr<Planner> planner; + /** \brief Appointments in the month that's being displayed + in the calendar section of the #Menu */ + std::shared_ptr<MonthPlanner> calendar_month; + + /** \brief The next appointments that follow highlighed date + highlighted in the calendar section of the #Menu + (default date = today) */ + std::shared_ptr<UpcomingPlanner> calendar_upcoming; /** \brief Configuration options that modify the view */ std::shared_ptr<Settings> settings; diff --git a/include/datetime/timezone-file.h b/include/datetime/timezone-file.h index d77aaae..a67c01a 100644 --- a/include/datetime/timezone-file.h +++ b/include/datetime/timezone-file.h @@ -42,8 +42,8 @@ public: ~FileTimezone(); private: - void setFilename(const std::string& filename); - static void onFileChanged(gpointer gself); + void set_filename(const std::string& filename); + static void on_file_changed(gpointer gself); void clear(); void reload(); diff --git a/po/POTFILES.in b/po/POTFILES.in index 1b1ee58..3faca0b 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -1,3 +1,5 @@ src/formatter.cpp src/formatter-desktop.cpp src/menu.cpp +src/snap.cpp +src/utils.c diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2cea786..9bc22f2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -13,15 +13,20 @@ add_library (${SERVICE_LIB} STATIC appointment.cpp clock.cpp clock-live.cpp + clock-watcher.cpp date-time.cpp + engine-eds.cpp exporter.cpp formatter.cpp formatter-desktop.cpp locations.cpp locations-settings.cpp menu.cpp - planner-eds.cpp + planner-month.cpp + planner-range.cpp + planner-upcoming.cpp settings-live.cpp + snap.cpp timezone-file.cpp timezone-geoclue.cpp timezones-live.cpp diff --git a/src/actions-live.cpp b/src/actions-live.cpp index f510ed1..97b12db 100644 --- a/src/actions-live.cpp +++ b/src/actions-live.cpp @@ -74,6 +74,30 @@ void LiveActions::open_desktop_settings() g_free (path); } +bool LiveActions::can_open_planner() const +{ + static bool inited = false; + static bool have_calendar = false; + + if (G_UNLIKELY(!inited)) + { + inited = true; + + auto all = g_app_info_get_all_for_type ("text/calendar"); + for(auto l=all; !have_calendar && l!=nullptr; l=l->next) + { + auto app_info = static_cast<GAppInfo*>(l->data); + + if (!g_strcmp0("evolution.desktop", g_app_info_get_id(app_info))) + have_calendar = true; + } + + g_list_free_full(all, (GDestroyNotify)g_object_unref); + } + + return have_calendar; +} + void LiveActions::open_planner() { execute_command("evolution -c calendar"); @@ -91,13 +115,15 @@ void LiveActions::open_phone_clock_app() void LiveActions::open_planner_at(const DateTime& dt) { - auto cmd = dt.format("evolution \"calendar:///?startdate=%Y%m%d\""); + const auto day_begins = dt.add_full(0, 0, 0, -dt.hour(), -dt.minute(), -dt.seconds()); + const auto gmt = day_begins.to_timezone("UTC"); + auto cmd = gmt.format("evolution \"calendar:///?startdate=%Y%m%dT%H%M%SZ\""); execute_command(cmd.c_str()); } void LiveActions::open_appointment(const std::string& uid) { - for(const auto& appt : state()->planner->upcoming.get()) + for(const auto& appt : state()->calendar_upcoming->appointments().get()) { if(appt.uid != uid) continue; @@ -156,7 +182,7 @@ on_datetime1_proxy_ready (GObject * object G_GNUC_UNUSED, GError * err = nullptr; auto proxy = g_dbus_proxy_new_for_bus_finish(res, &err); - if (err != NULL) + if (err != nullptr) { if (!g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_warning("Could not grab DBus proxy for timedated: %s", err->message); diff --git a/src/actions.cpp b/src/actions.cpp index d6fa698..c9c6286 100644 --- a/src/actions.cpp +++ b/src/actions.cpp @@ -65,7 +65,7 @@ void on_activate_appointment(GSimpleAction * /*action*/, g_return_if_fail(uid && *uid); // find url of the upcoming appointment with this uid - for (const auto& appt : self->state()->planner->upcoming.get()) + for (const auto& appt : self->state()->calendar_upcoming->appointments().get()) { if (appt.uid == uid) { @@ -146,7 +146,7 @@ GVariant* create_default_header_state() GVariant* create_calendar_state(const std::shared_ptr<State>& state) { gboolean days[32] = { 0 }; - for (const auto& appt : state->planner->this_month.get()) + for (const auto& appt : state->calendar_month->appointments().get()) days[appt.begin.day_of_month()] = true; GVariantBuilder day_builder; @@ -163,7 +163,7 @@ GVariant* create_calendar_state(const std::shared_ptr<State>& state) g_variant_builder_add(&dict_builder, "{sv}", key, v); key = "calendar-day"; - v = g_variant_new_int64(state->planner->time.get().to_unix()); + v = g_variant_new_int64(state->calendar_month->month().get().to_unix()); g_variant_builder_add(&dict_builder, "{sv}", key, v); key = "show-week-numbers"; @@ -219,10 +219,10 @@ Actions::Actions(const std::shared_ptr<State>& state): /// Keep our GActionGroup's action's states in sync with m_state /// - m_state->planner->time.changed().connect([this](const DateTime&){ + m_state->calendar_month->month().changed().connect([this](const DateTime&){ update_calendar_state(); }); - m_state->planner->this_month.changed().connect([this](const std::vector<Appointment>&){ + m_state->calendar_month->appointments().changed().connect([this](const std::vector<Appointment>&){ update_calendar_state(); }); m_state->settings->show_week_numbers.changed().connect([this](bool){ @@ -246,7 +246,8 @@ void Actions::update_calendar_state() void Actions::set_calendar_date(const DateTime& date) { - m_state->planner->time.set(date); + m_state->calendar_month->month().set(date); + m_state->calendar_upcoming->date().set(date); } GActionGroup* Actions::action_group() diff --git a/src/clock-watcher.cpp b/src/clock-watcher.cpp new file mode 100644 index 0000000..5da66c8 --- /dev/null +++ b/src/clock-watcher.cpp @@ -0,0 +1,81 @@ +/* + * Copyright 2014 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/clock-watcher.h> + +namespace unity { +namespace indicator { +namespace datetime { + +/*** +**** +***/ + +ClockWatcherImpl::ClockWatcherImpl(const std::shared_ptr<Clock>& clock, + const std::shared_ptr<UpcomingPlanner>& upcoming_planner): + m_clock(clock), + m_upcoming_planner(upcoming_planner) +{ + m_clock->date_changed.connect([this](){ + const auto now = m_clock->localtime(); + g_debug("ClockWatcher %p refretching appointments due to date change: %s", this, now.format("%F %T").c_str()); + m_upcoming_planner->date().set(now); + }); + + m_clock->minute_changed.connect([this](){ + g_debug("ClockWatcher %p calling pulse() due to clock minute_changed", this); + pulse(); + }); + + m_upcoming_planner->appointments().changed().connect([this](const std::vector<Appointment>&){ + g_debug("ClockWatcher %p calling pulse() due to appointments changed", this); + pulse(); + }); + + pulse(); +} + +core::Signal<const Appointment&>& ClockWatcherImpl::alarm_reached() +{ + return m_alarm_reached; +} + +void ClockWatcherImpl::pulse() +{ + const auto now = m_clock->localtime(); + + for(const auto& appointment : m_upcoming_planner->appointments().get()) + { + if (m_triggered.count(appointment.uid)) + continue; + if (!DateTime::is_same_minute(now, appointment.begin)) + continue; + + m_triggered.insert(appointment.uid); + m_alarm_reached(appointment); + } +} + +/*** +**** +***/ + +} // namespace datetime +} // namespace indicator +} // namespace unity diff --git a/src/date-time.cpp b/src/date-time.cpp index a634c5e..a1c1d1b 100644 --- a/src/date-time.cpp +++ b/src/date-time.cpp @@ -46,14 +46,22 @@ DateTime& DateTime::operator=(const DateTime& that) DateTime::DateTime(time_t t) { - GDateTime * gdt = g_date_time_new_from_unix_local(t); + auto gdt = g_date_time_new_from_unix_local(t); reset(gdt); g_date_time_unref(gdt); } DateTime DateTime::NowLocal() { - GDateTime * gdt = g_date_time_new_now_local(); + auto gdt = g_date_time_new_now_local(); + DateTime dt(gdt); + g_date_time_unref(gdt); + return dt; +} + +DateTime DateTime::Local(int year, int month, int day, int hour, int minute, int seconds) +{ + auto gdt = g_date_time_new_local (year, month, day, hour, minute, seconds); DateTime dt(gdt); g_date_time_unref(gdt); return dt; @@ -69,6 +77,14 @@ DateTime DateTime::to_timezone(const std::string& zone) const return dt; } +DateTime DateTime::add_full(int years, int months, int days, int hours, int minutes, double seconds) const +{ + auto gdt = g_date_time_add_full(get(), years, months, days, hours, minutes, seconds); + DateTime dt(gdt); + g_date_time_unref(gdt); + return dt; +} + GDateTime* DateTime::get() const { g_assert(m_dt); @@ -77,17 +93,43 @@ GDateTime* DateTime::get() const std::string DateTime::format(const std::string& fmt) const { - const auto str = g_date_time_format(get(), fmt.c_str()); - std::string ret = str; - g_free(str); + std::string ret; + + gchar* str = g_date_time_format(get(), fmt.c_str()); + if (str) + { + ret = str; + g_free(str); + } + return ret; } +void DateTime::ymd(int& year, int& month, int& day) const +{ + g_date_time_get_ymd(get(), &year, &month, &day); +} + int DateTime::day_of_month() const { return g_date_time_get_day_of_month(get()); } +int DateTime::hour() const +{ + return g_date_time_get_hour(get()); +} + +int DateTime::minute() const +{ + return g_date_time_get_minute(get()); +} + +double DateTime::seconds() const +{ + return g_date_time_get_seconds(get()); +} + int64_t DateTime::to_unix() const { return g_date_time_to_unix(get()); @@ -112,6 +154,11 @@ bool DateTime::operator<(const DateTime& that) const return g_date_time_compare(get(), that.get()) < 0; } +bool DateTime::operator<=(const DateTime& that) const +{ + return g_date_time_compare(get(), that.get()) <= 0; +} + bool DateTime::operator!=(const DateTime& that) const { // return true if this isn't set, or if it's not equal diff --git a/src/planner-eds.cpp b/src/engine-eds.cpp index cb42d6e..c557857 100644 --- a/src/planner-eds.cpp +++ b/src/engine-eds.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2013 Canonical Ltd. + * Copyright 2014 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 @@ -17,15 +17,17 @@ * Charles Kerr <charles.kerr@canonical.com> */ -#include <datetime/planner-eds.h> - -#include <datetime/appointment.h> +#include <datetime/engine-eds.h> #include <libical/ical.h> #include <libical/icaltime.h> #include <libecal/libecal.h> #include <libedataserver/libedataserver.h> +#include <algorithm> // std::sort() +#include <map> +#include <set> + namespace unity { namespace indicator { namespace datetime { @@ -34,25 +36,15 @@ namespace datetime { ***** ****/ -G_DEFINE_QUARK("source-client", source_client) - - -class PlannerEds::Impl +class EdsEngine::Impl { public: - Impl(PlannerEds& owner): + Impl(EdsEngine& owner): m_owner(owner), m_cancellable(g_cancellable_new()) { e_source_registry_new(m_cancellable, on_source_registry_ready, this); - - m_owner.time.changed().connect([this](const DateTime& dt) { - g_debug("planner's datetime property changed to %s; calling rebuildSoon()", dt.format("%F %T").c_str()); - rebuildSoon(); - }); - - rebuildSoon(); } ~Impl() @@ -60,6 +52,9 @@ public: g_cancellable_cancel(m_cancellable); g_clear_object(&m_cancellable); + while(!m_sources.empty()) + remove_source(*m_sources.begin()); + if (m_rebuild_tag) g_source_remove(m_rebuild_tag); @@ -68,8 +63,99 @@ public: g_clear_object(&m_source_registry); } + core::Signal<>& changed() + { + return m_changed; + } + + void get_appointments(const DateTime& begin, + const DateTime& end, + const Timezone& timezone, + std::function<void(const std::vector<Appointment>&)> func) + { + const auto begin_timet = begin.to_unix(); + const auto end_timet = end.to_unix(); + + const auto b_str = begin.format("%F %T"); + const auto e_str = end.format("%F %T"); + g_debug("getting all appointments from [%s ... %s]", b_str.c_str(), e_str.c_str()); + + /** + *** init the default timezone + **/ + + icaltimezone * default_timezone = nullptr; + const auto tz = timezone.timezone.get().c_str(); + if (tz && *tz) + { + default_timezone = icaltimezone_get_builtin_timezone(tz); + + if (default_timezone == nullptr) // maybe str is a tzid? + default_timezone = icaltimezone_get_builtin_timezone_from_tzid(tz); + + g_debug("default_timezone is %p", (void*)default_timezone); + } + + /** + *** walk through the sources to build the appointment list + **/ + + auto task_deleter = [](Task* task){ + // give the caller the (sorted) finished product + auto& a = task->appointments; + std::sort(a.begin(), a.end(), [](const Appointment& a, const Appointment& b){return a.begin < b.begin;}); + task->func(a); + // we're done; delete the task + g_debug("time to delete task %p", (void*)task); + delete task; + }; + + std::shared_ptr<Task> main_task(new Task(this, func), task_deleter); + + for (auto& kv : m_clients) + { + auto& client = kv.second; + if (default_timezone != nullptr) + e_cal_client_set_default_timezone(client, default_timezone); + + // start a new subtask to enumerate all the components in this client. + auto& source = kv.first; + auto extension = e_source_get_extension(source, E_SOURCE_EXTENSION_CALENDAR); + const auto color = e_source_selectable_get_color(E_SOURCE_SELECTABLE(extension)); + g_debug("calling e_cal_client_generate_instances for %p", (void*)client); + e_cal_client_generate_instances(client, + begin_timet, + end_timet, + m_cancellable, + my_get_appointments_foreach, + new AppointmentSubtask (main_task, client, color), + [](gpointer g){delete static_cast<AppointmentSubtask*>(g);}); + } + } + private: + void set_dirty_now() + { + m_changed(); + } + + static gboolean set_dirty_now_static (gpointer gself) + { + auto self = static_cast<Impl*>(gself); + self->m_rebuild_tag = 0; + self->set_dirty_now(); + return G_SOURCE_REMOVE; + } + + void set_dirty_soon() + { + static const int ARBITRARY_BATCH_MSEC = 200; + + if (m_rebuild_tag == 0) + m_rebuild_tag = g_timeout_add(ARBITRARY_BATCH_MSEC, set_dirty_now_static, this); + } + static void on_source_registry_ready(GObject* /*source*/, GAsyncResult* res, gpointer gself) { GError * error = nullptr; @@ -83,26 +169,31 @@ private: } else { - auto self = static_cast<Impl*>(gself); - - g_signal_connect(r, "source-added", G_CALLBACK(on_source_added), self); - g_signal_connect(r, "source-removed", G_CALLBACK(on_source_removed), self); - g_signal_connect(r, "source-changed", G_CALLBACK(on_source_changed), self); - g_signal_connect(r, "source-disabled", G_CALLBACK(on_source_disabled), self); - g_signal_connect(r, "source-enabled", G_CALLBACK(on_source_enabled), self); + g_signal_connect(r, "source-added", G_CALLBACK(on_source_added), gself); + g_signal_connect(r, "source-removed", G_CALLBACK(on_source_removed), gself); + g_signal_connect(r, "source-changed", G_CALLBACK(on_source_changed), gself); + g_signal_connect(r, "source-disabled", G_CALLBACK(on_source_disabled), gself); + g_signal_connect(r, "source-enabled", G_CALLBACK(on_source_enabled), gself); + auto self = static_cast<Impl*>(gself); self->m_source_registry = r; - - GList* sources = e_source_registry_list_sources(r, E_SOURCE_EXTENSION_CALENDAR); - for (auto l=sources; l!=nullptr; l=l->next) - on_source_added(r, E_SOURCE(l->data), gself); - g_list_free_full(sources, g_object_unref); + self->add_sources_by_extension(E_SOURCE_EXTENSION_CALENDAR); + self->add_sources_by_extension(E_SOURCE_EXTENSION_TASK_LIST); } } + void add_sources_by_extension(const char* extension) + { + auto& r = m_source_registry; + auto sources = e_source_registry_list_sources(r, extension); + for (auto l=sources; l!=nullptr; l=l->next) + on_source_added(r, E_SOURCE(l->data), this); + g_list_free_full(sources, g_object_unref); + } + static void on_source_added(ESourceRegistry* registry, ESource* source, gpointer gself) { - auto self = static_cast<PlannerEds::Impl*>(gself); + auto self = static_cast<Impl*>(gself); self->m_sources.insert(E_SOURCE(g_object_ref(source))); @@ -112,13 +203,35 @@ private: static void on_source_enabled(ESourceRegistry* /*registry*/, ESource* source, gpointer gself) { - auto self = static_cast<PlannerEds::Impl*>(gself); + auto self = static_cast<Impl*>(gself); + ECalClientSourceType source_type; + bool client_wanted = false; + + if (e_source_has_extension(source, E_SOURCE_EXTENSION_CALENDAR)) + { + source_type = E_CAL_CLIENT_SOURCE_TYPE_EVENTS; + client_wanted = true; + } + else if (e_source_has_extension(source, E_SOURCE_EXTENSION_TASK_LIST)) + { + source_type = E_CAL_CLIENT_SOURCE_TYPE_TASKS; + client_wanted = true; + } - e_cal_client_connect(source, - E_CAL_CLIENT_SOURCE_TYPE_EVENTS, - self->m_cancellable, - on_client_connected, - gself); + const auto source_uid = e_source_get_uid(source); + if (client_wanted) + { + g_debug("%s connecting a client to source %s", G_STRFUNC, source_uid); + e_cal_client_connect(source, + source_type, + self->m_cancellable, + on_client_connected, + gself); + } + else + { + g_debug("%s not using source %s -- no tasks/calendar", G_STRFUNC, source_uid); + } } static void on_client_connected(GObject* /*source*/, GAsyncResult * res, gpointer gself) @@ -134,45 +247,118 @@ private: } else { - // we've got a new connected ECalClient, so store it & notify clients - g_object_set_qdata_full(G_OBJECT(e_client_get_source(client)), - source_client_quark(), - client, - g_object_unref); - - g_debug("client connected; calling rebuildSoon()"); - static_cast<Impl*>(gself)->rebuildSoon(); + // add the client to our collection + auto self = static_cast<Impl*>(gself); + g_debug("got a client for %s", e_cal_client_get_local_attachment_store(E_CAL_CLIENT(client))); + self->m_clients[e_client_get_source(client)] = E_CAL_CLIENT(client); + + // now create a view for it so that we can listen for changes + e_cal_client_get_view (E_CAL_CLIENT(client), + "#t", // match all + self->m_cancellable, + on_client_view_ready, + self); + + g_debug("client connected; calling set_dirty_soon()"); + self->set_dirty_soon(); } } - static void on_source_disabled(ESourceRegistry* /*registry*/, ESource* source, gpointer gself) + static void on_client_view_ready (GObject* client, GAsyncResult* res, gpointer gself) { - gpointer e_cal_client; + GError* error = nullptr; + ECalClientView* view = nullptr; - // if this source has a connected ECalClient, remove it & notify clients - if ((e_cal_client = g_object_steal_qdata(G_OBJECT(source), source_client_quark()))) + if (e_cal_client_get_view_finish (E_CAL_CLIENT(client), res, &view, &error)) { - g_object_unref(e_cal_client); + // add the view to our collection + e_cal_client_view_start(view, &error); + g_debug("got a view for %s", e_cal_client_get_local_attachment_store(E_CAL_CLIENT(client))); + auto self = static_cast<Impl*>(gself); + self->m_views[e_client_get_source(E_CLIENT(client))] = view; - g_debug("source disabled; calling rebuildSoon()"); - static_cast<Impl*>(gself)->rebuildSoon(); + g_signal_connect(view, "objects-added", G_CALLBACK(on_view_objects_added), self); + g_signal_connect(view, "objects-modified", G_CALLBACK(on_view_objects_modified), self); + g_signal_connect(view, "objects-removed", G_CALLBACK(on_view_objects_removed), self); + g_debug("view connected; calling set_dirty_soon()"); + self->set_dirty_soon(); + } + else if(error != nullptr) + { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning("indicator-datetime cannot get View to EDS client: %s", error->message); + + g_error_free(error); } } - static void on_source_removed(ESourceRegistry* registry, ESource* source, gpointer gself) + static void on_view_objects_added(ECalClientView* /*view*/, gpointer /*objects*/, gpointer gself) { - auto self = static_cast<PlannerEds::Impl*>(gself); + g_debug("%s", G_STRFUNC); + static_cast<Impl*>(gself)->set_dirty_soon(); + } + static void on_view_objects_modified(ECalClientView* /*view*/, gpointer /*objects*/, gpointer gself) + { + g_debug("%s", G_STRFUNC); + static_cast<Impl*>(gself)->set_dirty_soon(); + } + static void on_view_objects_removed(ECalClientView* /*view*/, gpointer /*objects*/, gpointer gself) + { + g_debug("%s", G_STRFUNC); + static_cast<Impl*>(gself)->set_dirty_soon(); + } + + static void on_source_disabled(ESourceRegistry* /*registry*/, ESource* source, gpointer gself) + { + static_cast<Impl*>(gself)->disable_source(source); + } + void disable_source(ESource* source) + { + // if an ECalClientView is associated with this source, remove it + auto vit = m_views.find(source); + if (vit != m_views.end()) + { + auto& view = vit->second; + e_cal_client_view_stop(view, nullptr); + const auto n_disconnected = g_signal_handlers_disconnect_by_data(view, this); + g_warn_if_fail(n_disconnected == 3); + g_object_unref(view); + m_views.erase(vit); + set_dirty_soon(); + } + + // if an ECalClient is associated with this source, remove it + auto cit = m_clients.find(source); + if (cit != m_clients.end()) + { + auto& client = cit->second; + g_object_unref(client); + m_clients.erase(cit); + set_dirty_soon(); + } + } - on_source_disabled(registry, source, gself); + static void on_source_removed(ESourceRegistry* /*registry*/, ESource* source, gpointer gself) + { + static_cast<Impl*>(gself)->remove_source(source); + } + void remove_source(ESource* source) + { + disable_source(source); - self->m_sources.erase(source); - g_object_unref(source); + auto sit = m_sources.find(source); + if (sit != m_sources.end()) + { + g_object_unref(*sit); + m_sources.erase(sit); + set_dirty_soon(); + } } static void on_source_changed(ESourceRegistry* /*registry*/, ESource* /*source*/, gpointer gself) { - g_debug("source changed; calling rebuildSoon()"); - static_cast<Impl*>(gself)->rebuildSoon(); + g_debug("source changed; calling set_dirty_soon()"); + static_cast<Impl*>(gself)->set_dirty_soon(); } private: @@ -193,121 +379,12 @@ private: ECalClient* client; std::string color; AppointmentSubtask(const std::shared_ptr<Task>& task_in, ECalClient* client_in, const char* color_in): - task(task_in), client(client_in), color(color_in) {} - }; - - void rebuildSoon() - { - const static guint ARBITRARY_INTERVAL_SECS = 2; - - if (m_rebuild_tag == 0) - m_rebuild_tag = g_timeout_add_seconds(ARBITRARY_INTERVAL_SECS, rebuildNowStatic, this); - } - - static gboolean rebuildNowStatic(gpointer gself) - { - auto self = static_cast<Impl*>(gself); - self->m_rebuild_tag = 0; - self->rebuildNow(); - return G_SOURCE_REMOVE; - } - - void rebuildNow() - { - const auto calendar_date = m_owner.time.get().get(); - GDateTime* begin; - GDateTime* end; - int y, m, d; - - // get all the appointments in the calendar month - g_date_time_get_ymd(calendar_date, &y, &m, &d); - begin = g_date_time_new_local(y, m, 1, 0, 0, 0.1); - end = g_date_time_new_local(y, m, g_date_get_days_in_month(GDateMonth(m),GDateYear(y)), 23, 59, 59.9); - if (begin && end) + task(task_in), client(client_in) { - getAppointments(begin, end, [this](const std::vector<Appointment>& appointments) { - g_debug("got %d appointments in this calendar month", (int)appointments.size()); - m_owner.this_month.set(appointments); - }); + if (color_in) + color = color_in; } - g_clear_pointer(&begin, g_date_time_unref); - g_clear_pointer(&end, g_date_time_unref); - - // get the upcoming appointments - begin = g_date_time_ref(calendar_date); - end = g_date_time_add_months(begin, 1); - if (begin && end) - { - getAppointments(begin, end, [this](const std::vector<Appointment>& appointments) { - g_debug("got %d upcoming appointments", (int)appointments.size()); - m_owner.upcoming.set(appointments); - }); - } - g_clear_pointer(&begin, g_date_time_unref); - g_clear_pointer(&end, g_date_time_unref); - } - - void getAppointments(GDateTime* begin_dt, GDateTime* end_dt, appointment_func func) - { - const auto begin = g_date_time_to_unix(begin_dt); - const auto end = g_date_time_to_unix(end_dt); - - auto begin_str = g_date_time_format(begin_dt, "%F %T"); - auto end_str = g_date_time_format(end_dt, "%F %T"); - g_debug("getting all appointments from [%s ... %s]", begin_str, end_str); - g_free(begin_str); - g_free(end_str); - - /** - *** init the default timezone - **/ - - icaltimezone * default_timezone = nullptr; - - const auto tz = g_date_time_get_timezone_abbreviation(m_owner.time.get().get()); - g_debug("%s tz is %s", G_STRLOC, tz); - if (tz && *tz) - { - default_timezone = icaltimezone_get_builtin_timezone(tz); - - if (default_timezone == nullptr) // maybe str is a tzid? - default_timezone = icaltimezone_get_builtin_timezone_from_tzid(tz); - - g_debug("default_timezone is %p", (void*)default_timezone); - } - - /** - *** walk through the sources to build the appointment list - **/ - - std::shared_ptr<Task> main_task(new Task(this, func), [](Task* task){ - g_debug("time to delete task %p", (void*)task); - task->func(task->appointments); - delete task; - }); - - for (auto& source : m_sources) - { - auto client = E_CAL_CLIENT(g_object_get_qdata(G_OBJECT(source), source_client_quark())); - if (client == nullptr) - continue; - - if (default_timezone != nullptr) - e_cal_client_set_default_timezone(client, default_timezone); - - // start a new subtask to enumerate all the components in this client. - auto extension = e_source_get_extension(source, E_SOURCE_EXTENSION_CALENDAR); - const auto color = e_source_selectable_get_color(E_SOURCE_SELECTABLE(extension)); - g_debug("calling e_cal_client_generate_instances for %p", (void*)client); - e_cal_client_generate_instances(client, - begin, - end, - m_cancellable, - my_get_appointments_foreach, - new AppointmentSubtask (main_task, client, color), - [](gpointer g){delete static_cast<AppointmentSubtask*>(g);}); - } - } + }; struct UrlSubtask { @@ -355,14 +432,15 @@ private: e_cal_component_free_recur_list(recur_list); ECalComponentText text; - text.value = ""; + text.value = nullptr; e_cal_component_get_summary(component, &text); + if (text.value) + appointment.summary = text.value; appointment.begin = DateTime(begin); appointment.end = DateTime(end); appointment.color = subtask->color; appointment.is_event = vtype == E_CAL_COMPONENT_EVENT; - appointment.summary = text.value; appointment.uid = uid; GList * alarm_uids = e_cal_component_get_alarm_uids(component); @@ -390,8 +468,11 @@ private: e_cal_client_get_attachment_uris_finish(E_CAL_CLIENT(client), res, &uris, &error); if (error != nullptr) { - if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && + !g_error_matches(error, E_CLIENT_ERROR, E_CLIENT_ERROR_NOT_SUPPORTED)) + { g_warning("Error getting appointment uris: %s", error->message); + } g_error_free(error); } @@ -407,18 +488,43 @@ private: delete subtask; } -private: - - PlannerEds& m_owner; + EdsEngine& m_owner; + core::Signal<> m_changed; std::set<ESource*> m_sources; - GCancellable * m_cancellable = nullptr; - ESourceRegistry * m_source_registry = nullptr; + std::map<ESource*,ECalClient*> m_clients; + std::map<ESource*,ECalClientView*> m_views; + GCancellable* m_cancellable = nullptr; + ESourceRegistry* m_source_registry = nullptr; guint m_rebuild_tag = 0; }; -PlannerEds::PlannerEds(): p(new Impl(*this)) {} +/*** +**** +***/ + +EdsEngine::EdsEngine(): + p(new Impl(*this)) +{ +} + +EdsEngine::~EdsEngine() =default; + +core::Signal<>& EdsEngine::changed() +{ + return p->changed(); +} + +void EdsEngine::get_appointments(const DateTime& begin, + const DateTime& end, + const Timezone& tz, + std::function<void(const std::vector<Appointment>&)> func) +{ + p->get_appointments(begin, end, tz, func); +} -PlannerEds::~PlannerEds() =default; +/*** +**** +***/ } // namespace datetime } // namespace indicator diff --git a/src/main.cpp b/src/main.cpp index 1534777..c7b35e5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,24 +17,28 @@ * with this program. If not, see <http://www.gnu.org/licenses/>. */ - - #include <datetime/actions-live.h> #include <datetime/clock.h> +#include <datetime/clock-watcher.h> +#include <datetime/engine-mock.h> +#include <datetime/engine-eds.h> #include <datetime/exporter.h> #include <datetime/locations-settings.h> #include <datetime/menu.h> -#include <datetime/planner-eds.h> +#include <datetime/planner-range.h> #include <datetime/settings-live.h> +#include <datetime/snap.h> #include <datetime/state.h> +#include <datetime/timezone-file.h> #include <datetime/timezones-live.h> #include <glib/gi18n.h> // bindtextdomain() #include <gio/gio.h> -#include <libnotify/notify.h> + +#include <url-dispatcher.h> #include <locale.h> -#include <stdlib.h> // exit() +#include <cstdlib> // exit() using namespace unity::indicator::datetime; @@ -50,23 +54,47 @@ main(int /*argc*/, char** /*argv*/) bindtextdomain(GETTEXT_PACKAGE, GNOMELOCALEDIR); textdomain(GETTEXT_PACKAGE); - // init libnotify - if(!notify_init("indicator-datetime-service")) - g_critical("libnotify initialization failed"); + // we don't show appointments in the greeter, + // so no need to connect to EDS there... + std::shared_ptr<Engine> engine; + if (!g_strcmp0("lightdm", g_get_user_name())) + engine.reset(new MockEngine); + else + engine.reset(new EdsEngine); // build the state, actions, and menufactory std::shared_ptr<State> state(new State); std::shared_ptr<Settings> live_settings(new LiveSettings); std::shared_ptr<Timezones> live_timezones(new LiveTimezones(live_settings, TIMEZONE_FILE)); std::shared_ptr<Clock> live_clock(new LiveClock(live_timezones)); + std::shared_ptr<Timezone> file_timezone(new FileTimezone(TIMEZONE_FILE)); + const auto now = live_clock->localtime(); state->settings = live_settings; state->clock = live_clock; state->locations.reset(new SettingsLocations(live_settings, live_timezones)); - state->planner.reset(new PlannerEds); - state->planner->time = live_clock->localtime(); + auto calendar_month = new MonthPlanner(std::shared_ptr<RangePlanner>(new SimpleRangePlanner(engine, file_timezone)), now); + state->calendar_month.reset(calendar_month); + state->calendar_upcoming.reset(new UpcomingPlanner(std::shared_ptr<RangePlanner>(new SimpleRangePlanner(engine, file_timezone)), now)); std::shared_ptr<Actions> actions(new LiveActions(state)); MenuFactory factory(actions, state); + // snap decisions + std::shared_ptr<UpcomingPlanner> upcoming_planner(new UpcomingPlanner(std::shared_ptr<RangePlanner>(new SimpleRangePlanner(engine, file_timezone)), now)); + ClockWatcherImpl clock_watcher(live_clock, upcoming_planner); + Snap snap; + clock_watcher.alarm_reached().connect([&snap](const Appointment& appt){ + auto snap_show = [](const Appointment& a){ + const char* url; + if(!a.url.empty()) + url = a.url.c_str(); + else // alarm doesn't have a URl associated with it; use a fallback + url = "appid://com.ubuntu.clock/clock/current-user-version"; + url_dispatch_send(url, nullptr, nullptr); + }; + auto snap_dismiss = [](const Appointment&){}; + snap(appt, snap_show, snap_dismiss); + }); + // create the menus std::vector<std::shared_ptr<Menu>> menus; for(int i=0, n=Menu::NUM_PROFILES; i<n; i++) diff --git a/src/menu.cpp b/src/menu.cpp index bdf92c3..90ef41f 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -22,11 +22,11 @@ #include <datetime/formatter.h> #include <datetime/state.h> -#include <json-glib/json-glib.h> - #include <glib/gi18n.h> #include <gio/gio.h> +#include <vector> + namespace unity { namespace indicator { namespace datetime { @@ -62,7 +62,7 @@ GMenuModel* Menu::menu_model() ****/ -#define FALLBACK_ALARM_CLOCK_ICON_NAME "clock" +#define ALARM_ICON_NAME "alarm-clock" #define CALENDAR_ICON_NAME "calendar" class MenuImpl: public Menu @@ -104,13 +104,16 @@ protected: m_state->settings->show_events.changed().connect([this](bool){ update_section(Appointments); // showing events got toggled }); - m_state->planner->upcoming.changed().connect([this](const std::vector<Appointment>&){ - update_section(Appointments); // "upcoming" is the list of Appointments we show + m_state->calendar_upcoming->appointments().changed().connect([this](const std::vector<Appointment>&){ + update_upcoming(); // our m_upcoming is planner->upcoming() filtered by time }); m_state->clock->date_changed.connect([this](){ update_section(Calendar); // need to update the Date menuitem update_section(Locations); // locations' relative time may have changed }); + m_state->clock->minute_changed.connect([this](){ + update_upcoming(); // our m_upcoming is planner->upcoming() filtered by time + }); m_state->locations->locations.changed().connect([this](const std::vector<Location>&) { update_section(Locations); // "locations" is the list of Locations we show }); @@ -133,6 +136,30 @@ protected: g_action_group_change_action_state(action_group, action_name.c_str(), state); } + void update_upcoming() + { + // show upcoming appointments that occur after "calendar_next_minute", + // where that is the wallclock time on the specified calendar day + const auto calendar_day = m_state->calendar_month->month().get(); + const auto now = m_state->clock->localtime(); + int y, m, d; + calendar_day.ymd(y, m, d); + const auto calendar_now = DateTime::Local(y, m, d, now.hour(), now.minute(), now.seconds()); + const auto calendar_next_minute = calendar_now.add_full(0, 0, 0, 0, 1, -now.seconds()); + + std::vector<Appointment> upcoming; + for(const auto& a : m_state->calendar_upcoming->appointments().get()) + if (calendar_next_minute <= a.begin) + upcoming.push_back(a); + + if (m_upcoming != upcoming) + { + m_upcoming.swap(upcoming); + update_header(); // show an 'alarm' icon if there are upcoming alarms + update_section(Appointments); // "upcoming" is the list of Appointments we show + } + } + std::shared_ptr<const State> m_state; std::shared_ptr<Actions> m_actions; std::shared_ptr<const Formatter> m_formatter; @@ -141,71 +168,19 @@ protected: GVariant* get_serialized_alarm_icon() { if (G_UNLIKELY(m_serialized_alarm_icon == nullptr)) - m_serialized_alarm_icon = create_alarm_icon(); - - return m_serialized_alarm_icon; - } - -private: - - /* try to get the clock app's filename from click. (/$pkgdir/$icon) */ - static GVariant* create_alarm_icon() - { - GVariant* serialized = nullptr; - gchar* icon_filename = nullptr; - gchar* standard_error = nullptr; - gchar* pkgdir = nullptr; - - g_spawn_command_line_sync("click pkgdir com.ubuntu.clock", &pkgdir, &standard_error, nullptr, nullptr); - g_clear_pointer(&standard_error, g_free); - if (pkgdir != nullptr) - { - gchar* manifest = nullptr; - g_strstrip(pkgdir); - g_spawn_command_line_sync("click info com.ubuntu.clock", &manifest, &standard_error, nullptr, nullptr); - g_clear_pointer(&standard_error, g_free); - if (manifest != nullptr) - { - JsonParser* parser = json_parser_new(); - if (json_parser_load_from_data(parser, manifest, -1, nullptr)) - { - JsonNode* root = json_parser_get_root(parser); /* transfer-none */ - if ((root != nullptr) && (JSON_NODE_TYPE(root) == JSON_NODE_OBJECT)) - { - JsonObject* o = json_node_get_object(root); /* transfer-none */ - const gchar* icon_name = json_object_get_string_member(o, "icon"); - if (icon_name != nullptr) - icon_filename = g_build_filename(pkgdir, icon_name, nullptr); - } - } - g_object_unref(parser); - g_free(manifest); - } - g_free(pkgdir); - } - - if (icon_filename != nullptr) - { - GFile* file = g_file_new_for_path(icon_filename); - GIcon* icon = g_file_icon_new(file); - - serialized = g_icon_serialize(icon); - - g_object_unref(icon); - g_object_unref(file); - g_free(icon_filename); - } - - if (serialized == nullptr) { - auto i = g_themed_icon_new_with_default_fallbacks(FALLBACK_ALARM_CLOCK_ICON_NAME); - serialized = g_icon_serialize(i); + auto i = g_themed_icon_new_with_default_fallbacks(ALARM_ICON_NAME); + m_serialized_alarm_icon = g_icon_serialize(i); g_object_unref(i); } - return serialized; + return m_serialized_alarm_icon; } + std::vector<Appointment> m_upcoming; + +private: + GVariant* get_serialized_calendar_icon() { if (G_UNLIKELY(m_serialized_calendar_icon == nullptr)) @@ -273,7 +248,7 @@ private: // add calendar if (show_calendar) { - item = g_menu_item_new ("[calendar]", NULL); + item = g_menu_item_new ("[calendar]", nullptr); v = g_variant_new_int64(0); g_menu_item_set_action_and_target_value (item, "indicator.calendar", v); g_menu_item_set_attribute (item, "x-canonical-type", @@ -292,18 +267,19 @@ private: void add_appointments(GMenu* menu, Profile profile) { - int n = 0; const int MAX_APPTS = 5; std::set<std::string> added; - for (const auto& appt : m_state->planner->upcoming.get()) + for (const auto& appt : m_upcoming) { - if (n++ >= MAX_APPTS) - break; - + // don't show duplicates if (added.count(appt.uid)) continue; + // don't show too many + if (g_menu_model_get_n_items (G_MENU_MODEL(menu)) >= MAX_APPTS) + break; + added.insert(appt.uid); GDateTime* begin = appt.begin(); @@ -332,7 +308,7 @@ private: g_menu_item_set_action_and_target_value (menu_item, "indicator.activate-appointment", g_variant_new_string (appt.uid.c_str())); - else + else if (m_actions->can_open_planner()) g_menu_item_set_action_and_target_value (menu_item, "indicator.activate-planner", g_variant_new_int64 (unix_time)); @@ -349,13 +325,16 @@ private: { add_appointments (menu, profile); - // add the 'Add Event…' menuitem - auto menu_item = g_menu_item_new(_("Add Event…"), nullptr); - const gchar* action_name = "indicator.activate-planner"; - auto v = g_variant_new_int64(0); - g_menu_item_set_action_and_target_value(menu_item, action_name, v); - g_menu_append_item(menu, menu_item); - g_object_unref(menu_item); + if (m_actions->can_open_planner()) + { + // add the 'Add Event…' menuitem + auto menu_item = g_menu_item_new(_("Add Event…"), nullptr); + const gchar* action_name = "indicator.activate-planner"; + auto v = g_variant_new_int64(0); + g_menu_item_set_action_and_target_value(menu_item, action_name, v); + g_menu_append_item(menu, menu_item); + g_object_unref(menu_item); + } } else if (profile==Phone) { @@ -508,7 +487,7 @@ protected: { // are there alarms? bool has_alarms = false; - for(const auto& appointment : m_state->planner->upcoming.get()) + for(const auto& appointment : m_upcoming) if((has_alarms = appointment.has_alarms)) break; diff --git a/src/planner-month.cpp b/src/planner-month.cpp new file mode 100644 index 0000000..5920daa --- /dev/null +++ b/src/planner-month.cpp @@ -0,0 +1,66 @@ +/* + * Copyright 2014 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/planner-month.h> + +namespace unity { +namespace indicator { +namespace datetime { + +/*** +**** +***/ + +MonthPlanner::MonthPlanner(const std::shared_ptr<RangePlanner>& range_planner, + const DateTime& month_in): + m_range_planner(range_planner) +{ + month().changed().connect([this](const DateTime& m){ + auto month_begin = m.add_full(0, // no years + 0, // no months + -(m.day_of_month()-1), + -m.hour(), + -m.minute(), + -m.seconds()); + auto month_end = month_begin.add_full(0, 1, 0, 0, 0, -0.1); + g_debug("PlannerMonth %p setting calendar month range: [%s..%s]", this, month_begin.format("%F %T").c_str(), month_end.format("%F %T").c_str()); + m_range_planner->range().set(std::pair<DateTime,DateTime>(month_begin,month_end)); + }); + + month().set(month_in); +} + +core::Property<DateTime>& MonthPlanner::month() +{ + return m_month; +} + +core::Property<std::vector<Appointment>>& MonthPlanner::appointments() +{ + return m_range_planner->appointments(); +} + + +/*** +**** +***/ + +} // namespace datetime +} // namespace indicator +} // namespace unity diff --git a/src/planner-range.cpp b/src/planner-range.cpp new file mode 100644 index 0000000..93946e0 --- /dev/null +++ b/src/planner-range.cpp @@ -0,0 +1,105 @@ +/* + * Copyright 2014 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/planner-range.h> + +namespace unity { +namespace indicator { +namespace datetime { + +/*** +**** +***/ + +SimpleRangePlanner::SimpleRangePlanner(const std::shared_ptr<Engine>& engine, + const std::shared_ptr<Timezone>& timezone): + m_engine(engine), + m_timezone(timezone), + m_range(std::pair<DateTime,DateTime>(DateTime::NowLocal(), DateTime::NowLocal())) +{ + engine->changed().connect([this](){ + g_debug("RangePlanner %p rebuilding soon because Engine %p emitted 'changed' signal%p", this, m_engine.get()); + rebuild_soon(); + }); + + range().changed().connect([this](const std::pair<DateTime,DateTime>&){ + g_debug("rebuilding because the date range changed"); + rebuild_soon(); + }); +} + +SimpleRangePlanner::~SimpleRangePlanner() +{ + if (m_rebuild_tag) + g_source_remove(m_rebuild_tag); +} + +/*** +**** +***/ + +void SimpleRangePlanner::rebuild_now() +{ + const auto& r = range().get(); + + auto on_appointments_fetched = [this](const std::vector<Appointment>& a){ + g_debug("RangePlanner %p got %zu appointments", this, a.size()); + appointments().set(a); + }; + + m_engine->get_appointments(r.first, r.second, *m_timezone.get(), on_appointments_fetched); +} + +void SimpleRangePlanner::rebuild_soon() +{ + static const int ARBITRARY_BATCH_MSEC = 200; + + if (m_rebuild_tag == 0) + m_rebuild_tag = g_timeout_add(ARBITRARY_BATCH_MSEC, rebuild_now_static, this); +} + +gboolean SimpleRangePlanner::rebuild_now_static(gpointer gself) +{ + auto self = static_cast<SimpleRangePlanner*>(gself); + self->m_rebuild_tag = 0; + self->rebuild_now(); + return G_SOURCE_REMOVE; +} + +/*** +**** +***/ + +core::Property<std::vector<Appointment>>& SimpleRangePlanner::appointments() +{ + return m_appointments; +} + +core::Property<std::pair<DateTime,DateTime>>& SimpleRangePlanner::range() +{ + return m_range; +} + +/*** +**** +***/ + +} // namespace datetime +} // namespace indicator +} // namespace unity diff --git a/src/planner-upcoming.cpp b/src/planner-upcoming.cpp new file mode 100644 index 0000000..4e5af6f --- /dev/null +++ b/src/planner-upcoming.cpp @@ -0,0 +1,61 @@ +/* + * Copyright 2014 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/planner-upcoming.h> + +namespace unity { +namespace indicator { +namespace datetime { + +/*** +**** +***/ + +UpcomingPlanner::UpcomingPlanner(const std::shared_ptr<RangePlanner>& range_planner, + const DateTime& date_in): + m_range_planner(range_planner) +{ + date().changed().connect([this](const DateTime& dt){ + // set the range to the upcoming month + const auto b = dt.add_full(0, 0, -1, 0, 0, 0); + const auto e = dt.add_full(0, 1, 0, 0, 0, 0); + g_debug("%p setting date range to [%s..%s]", this, b.format("%F %T").c_str(), e.format("%F %T").c_str()); + m_range_planner->range().set(std::pair<DateTime,DateTime>(b,e)); + }); + + date().set(date_in); +} + +core::Property<DateTime>& UpcomingPlanner::date() +{ + return m_date; +} + +core::Property<std::vector<Appointment>>& UpcomingPlanner::appointments() +{ + return m_range_planner->appointments(); +} + +/*** +**** +***/ + +} // namespace datetime +} // namespace indicator +} // namespace unity diff --git a/src/snap.cpp b/src/snap.cpp new file mode 100644 index 0000000..3ab2941 --- /dev/null +++ b/src/snap.cpp @@ -0,0 +1,328 @@ +/* + * Copyright 2014 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/appointment.h> +#include <datetime/formatter.h> +#include <datetime/snap.h> + +#include <canberra.h> +#include <libnotify/notify.h> + +#include <glib/gi18n.h> +#include <glib.h> + +#include <set> +#include <string> + +#define ALARM_SOUND_FILENAME "/usr/share/sounds/ubuntu/stereo/phone-incoming-call.ogg" + +namespace unity { +namespace indicator { +namespace datetime { + +/*** +**** +***/ + +namespace +{ + +/** +*** libcanberra -- play sounds +**/ + +// arbitrary number, but we need a consistent id for play/cancel +const int32_t alarm_ca_id = 1; + +gboolean media_cached = FALSE; +ca_context *c_context = nullptr; +guint timeout_tag = 0; + +ca_context* get_ca_context() +{ + if (G_UNLIKELY(c_context == nullptr)) + { + int rv; + + if ((rv = ca_context_create(&c_context)) != CA_SUCCESS) + { + g_warning("Failed to create canberra context: %s\n", ca_strerror(rv)); + c_context = nullptr; + } + else + { + const char* filename = ALARM_SOUND_FILENAME; + rv = ca_context_cache(c_context, + CA_PROP_EVENT_ID, "alarm", + CA_PROP_MEDIA_FILENAME, filename, + CA_PROP_CANBERRA_CACHE_CONTROL, "permanent", + NULL); + media_cached = rv == CA_SUCCESS; + if (rv != CA_SUCCESS) + g_warning("Couldn't add '%s' to canberra cache: %s", filename, ca_strerror(rv)); + } + } + + return c_context; +} + +void play_alarm_sound(); + +gboolean play_alarm_sound_idle (gpointer) +{ + timeout_tag = 0; + play_alarm_sound(); + return G_SOURCE_REMOVE; +} + +void on_alarm_play_done (ca_context* /*context*/, uint32_t /*id*/, int rv, void* /*user_data*/) +{ + // wait one second, then play it again + if ((rv == CA_SUCCESS) && (timeout_tag == 0)) + timeout_tag = g_timeout_add_seconds (1, play_alarm_sound_idle, nullptr); +} + +void play_alarm_sound() +{ + const gchar* filename = ALARM_SOUND_FILENAME; + auto context = get_ca_context(); + g_return_if_fail(context != nullptr); + + ca_proplist* props = nullptr; + ca_proplist_create(&props); + if (media_cached) + ca_proplist_sets(props, CA_PROP_EVENT_ID, "alarm"); + ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, filename); + + const auto rv = ca_context_play_full(context, alarm_ca_id, props, on_alarm_play_done, nullptr); + if (rv != CA_SUCCESS) + g_warning("Failed to play file '%s': %s", filename, ca_strerror(rv)); + + g_clear_pointer(&props, ca_proplist_destroy); +} + +void stop_alarm_sound() +{ + auto context = get_ca_context(); + if (context != nullptr) + { + const auto rv = ca_context_cancel(context, alarm_ca_id); + if (rv != CA_SUCCESS) + g_warning("Failed to cancel alarm sound: %s", ca_strerror(rv)); + } + + if (timeout_tag != 0) + { + g_source_remove(timeout_tag); + timeout_tag = 0; + } +} + +/** +*** libnotify -- snap decisions +**/ + +void first_time_init() +{ + static bool inited = false; + + if (G_UNLIKELY(!inited)) + { + inited = true; + + if(!notify_init("indicator-datetime-service")) + g_critical("libnotify initialization failed"); + } +} + +struct SnapData +{ + Snap::appointment_func show; + Snap::appointment_func dismiss; + Appointment appointment; +}; + +void on_snap_show(NotifyNotification*, gchar* /*action*/, gpointer gdata) +{ + stop_alarm_sound(); + auto data = static_cast<SnapData*>(gdata); + data->show(data->appointment); +} + +void on_snap_dismiss(NotifyNotification*, gchar* /*action*/, gpointer gdata) +{ + stop_alarm_sound(); + auto data = static_cast<SnapData*>(gdata); + data->dismiss(data->appointment); +} + +void on_snap_closed(NotifyNotification*, gpointer) +{ + stop_alarm_sound(); +} + +void snap_data_destroy_notify(gpointer gdata) +{ + delete static_cast<SnapData*>(gdata); +} + +std::set<std::string> get_server_caps() +{ + std::set<std::string> caps_set; + auto caps_gl = notify_get_server_caps(); + std::string caps_str; + for(auto l=caps_gl; l!=nullptr; l=l->next) + { + caps_set.insert((const char*)l->data); + + caps_str += (const char*) l->data;; + if (l->next != nullptr) + caps_str += ", "; + } + g_debug ("%s notify_get_server() returned [%s]", G_STRFUNC, caps_str.c_str()); + g_list_free_full(caps_gl, g_free); + return caps_set; +} + +typedef enum +{ + // just a bubble... no actions, no audio + NOTIFY_MODE_BUBBLE, + + // a snap decision popup dialog + audio + NOTIFY_MODE_SNAP +} +NotifyMode; + +NotifyMode get_notify_mode() +{ + static NotifyMode mode; + static bool mode_inited = false; + + if (G_UNLIKELY(!mode_inited)) + { + const auto caps = get_server_caps(); + + if (caps.count("actions")) + mode = NOTIFY_MODE_SNAP; + else + mode = NOTIFY_MODE_BUBBLE; + + mode_inited = true; + } + + return mode; +} + +bool show_notification (SnapData* data, NotifyMode mode) +{ + const Appointment& appointment = data->appointment; + + const auto timestr = appointment.begin.format("%a, %X"); + auto title = g_strdup_printf(_("Alarm %s"), timestr.c_str()); + const auto body = appointment.summary; + const gchar* icon_name = "alarm-clock"; + + auto nn = notify_notification_new(title, body.c_str(), icon_name); + if (mode == NOTIFY_MODE_SNAP) + { + notify_notification_set_hint_string(nn, "x-canonical-snap-decisions", "true"); + notify_notification_set_hint_string(nn, "x-canonical-private-button-tint", "true"); + notify_notification_add_action(nn, "show", _("Show"), on_snap_show, data, nullptr); + notify_notification_add_action(nn, "dismiss", _("Dismiss"), on_snap_dismiss, data, nullptr); + g_signal_connect(G_OBJECT(nn), "closed", G_CALLBACK(on_snap_closed), data); + } + g_object_set_data_full(G_OBJECT(nn), "snap-data", data, snap_data_destroy_notify); + + bool shown = true; + GError * error = nullptr; + notify_notification_show(nn, &error); + if (error != NULL) + { + g_critical("Unable to show snap decision for '%s': %s", body.c_str(), error->message); + g_error_free(error); + data->show(data->appointment); + shown = false; + } + + g_free(title); + return shown; +} + +/** +*** +**/ + +void notify(const Appointment& appointment, + Snap::appointment_func show, + Snap::appointment_func dismiss) +{ + auto data = new SnapData; + data->appointment = appointment; + data->show = show; + data->dismiss = dismiss; + + switch (get_notify_mode()) + { + case NOTIFY_MODE_BUBBLE: + show_notification(data, NOTIFY_MODE_BUBBLE); + break; + + default: + if (show_notification(data, NOTIFY_MODE_SNAP)) + play_alarm_sound(); + break; + } +} + +} // unnamed namespace + + +/*** +**** +***/ + +Snap::Snap() +{ + first_time_init(); +} + +Snap::~Snap() +{ + media_cached = false; + g_clear_pointer(&c_context, ca_context_destroy); +} + +void Snap::operator()(const Appointment& appointment, + appointment_func show, + appointment_func dismiss) +{ + if (appointment.has_alarms) + notify(appointment, show, dismiss); + else + dismiss(appointment); +} + +/*** +**** +***/ + +} // namespace datetime +} // namespace indicator +} // namespace unity diff --git a/src/timezone-file.cpp b/src/timezone-file.cpp index 76737b4..c99897a 100644 --- a/src/timezone-file.cpp +++ b/src/timezone-file.cpp @@ -19,6 +19,9 @@ #include <datetime/timezone-file.h> +#include <cerrno> +#include <cstdlib> + namespace unity { namespace indicator { namespace datetime { @@ -29,7 +32,7 @@ FileTimezone::FileTimezone() FileTimezone::FileTimezone(const std::string& filename) { - setFilename(filename); + set_filename(filename); } FileTimezone::~FileTimezone() @@ -49,13 +52,23 @@ FileTimezone::clear() } void -FileTimezone::setFilename(const std::string& filename) +FileTimezone::set_filename(const std::string& filename) { clear(); - m_filename = filename; + auto tmp = realpath(filename.c_str(), nullptr); + if(tmp != nullptr) + { + m_filename = tmp; + free(tmp); + } + else + { + g_warning("Unable to resolve path '%s': %s", filename.c_str(), g_strerror(errno)); + m_filename = filename; // better than nothing? + } - auto file = g_file_new_for_path(filename.c_str()); + auto file = g_file_new_for_path(m_filename.c_str()); GError * err = nullptr; m_monitor = g_file_monitor_file(file, G_FILE_MONITOR_NONE, nullptr, &err); g_object_unref(file); @@ -66,15 +79,15 @@ FileTimezone::setFilename(const std::string& filename) } else { - m_monitor_handler_id = g_signal_connect_swapped(m_monitor, "changed", G_CALLBACK(onFileChanged), this); - g_debug("%s Monitoring timezone file '%s'", G_STRLOC, filename.c_str()); + m_monitor_handler_id = g_signal_connect_swapped(m_monitor, "changed", G_CALLBACK(on_file_changed), this); + g_debug("%s Monitoring timezone file '%s'", G_STRLOC, m_filename.c_str()); } reload(); } void -FileTimezone::onFileChanged(gpointer gself) +FileTimezone::on_file_changed(gpointer gself) { static_cast<FileTimezone*>(gself)->reload(); } diff --git a/src/utils.c b/src/utils.c index f4eb53f..c9107ce 100644 --- a/src/utils.c +++ b/src/utils.c @@ -159,6 +159,10 @@ getDateProximity(GDateTime* now, GDateTime* time) gint now_year, now_month, now_day; gint time_year, time_month, time_day; + // did it already happen? + if (g_date_time_difference(time, now) < -G_USEC_PER_SEC) + return DATE_PROXIMITY_FAR; + // 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); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3dcd151..7d590c9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -42,6 +42,7 @@ function(add_test_by_name name) endfunction() add_test_by_name(test-actions) add_test_by_name(test-clock) +add_test_by_name(test-clock-watcher) add_test_by_name(test-exporter) add_test_by_name(test-formatter) add_test_by_name(test-live-actions) @@ -52,6 +53,10 @@ add_test_by_name(test-settings) add_test_by_name(test-timezone-file) add_test_by_name(test-utils) +set (TEST_NAME manual-test-snap) +add_executable (${TEST_NAME} ${TEST_NAME}.cpp) +add_dependencies (${TEST_NAME} libindicatordatetimeservice) +target_link_libraries (${TEST_NAME} indicatordatetimeservice gtest ${SERVICE_DEPS_LIBRARIES} ${GTEST_LIBS}) # disabling the timezone unit tests because they require # https://code.launchpad.net/~ted/dbus-test-runner/multi-interface-test/+merge/199724 diff --git a/tests/actions-mock.h b/tests/actions-mock.h index da93cb9..ebd8a4d 100644 --- a/tests/actions-mock.h +++ b/tests/actions-mock.h @@ -49,6 +49,8 @@ public: void open_phone_clock_app() { m_history.push_back(OpenPhoneClockApp); } + bool can_open_planner() const { return m_can_open_planner; } + void open_planner() { m_history.push_back(OpenPlanner); } void open_planner_at(const DateTime& date_time_) { @@ -67,7 +69,10 @@ public: m_url = url_; } + void set_can_open_planner(bool b) { m_can_open_planner = b; } + private: + bool m_can_open_planner = true; std::string m_url; std::string m_zone; std::string m_name; diff --git a/tests/geoclue-fixture.h b/tests/geoclue-fixture.h index 7e29018..0c597d3 100644 --- a/tests/geoclue-fixture.h +++ b/tests/geoclue-fixture.h @@ -95,7 +95,7 @@ class GeoclueFixture : public GlibFixture // I've looked and can't find where this extra ref is coming from. // is there an unbalanced ref to the bus in the test harness?! - while (bus != NULL) + while (bus != nullptr) { g_object_unref (bus); wait_msec (1000); diff --git a/tests/manual b/tests/manual new file mode 100644 index 0000000..17b4778 --- /dev/null +++ b/tests/manual @@ -0,0 +1,24 @@ + +Test-case indicator-datetime/unity7-items-check +<dl> + <dt>Log in to a Unity 7 user session</dt> + <dt>Go to the panel and click on the DateTime indicator</dt> + <dd>Ensure there are items in the menu</dd> +</dl> + +Test-case indicator-datetime/unity7-greeter-items-check +<dl> + <dt>Start a system and wait for the greeter or logout of the current user session</dt> + <dt>Go to the panel and click on the DateTime indicator</dt> + <dd>Ensure there are items in the menu</dd> +</dl> + +Test-case indicator-datetime/unity8-items-check +<dl> + <dt>Login to a user session running Unity 8</dt> + <dt>Pull down the top panel until it sticks open</dt> + <dt>Navigate through the tabs until "Upcoming" is shown</dt> + <dd>Upcoming is at the top of the menu</dd> + <dd>The menu is populated with items</dd> +</dl> + diff --git a/tests/manual-test-snap.cpp b/tests/manual-test-snap.cpp new file mode 100644 index 0000000..51556cd --- /dev/null +++ b/tests/manual-test-snap.cpp @@ -0,0 +1,63 @@ + +/* + * 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 <datetime/appointment.h> +#include <datetime/snap.h> + +#include <glib.h> + +using namespace unity::indicator::datetime; + +/*** +**** +***/ + +int main() +{ + Appointment a; + a.color = "green"; + a.summary = "Alarm"; + a.url = "alarm:///hello-world"; + a.uid = "D4B57D50247291478ED31DED17FF0A9838DED402"; + a.is_event = false; + a.is_daily = false; + a.has_alarms = true; + auto begin = g_date_time_new_local(2014,12,25,0,0,0); + auto end = g_date_time_add_full(begin,0,0,1,0,0,-1); + a.begin = begin; + a.end = end; + g_date_time_unref(end); + g_date_time_unref(begin); + + auto loop = g_main_loop_new(nullptr, false); + auto show = [loop](const Appointment& appt){ + g_message("You clicked 'show' for appt url '%s'", appt.url.c_str()); + g_main_loop_quit(loop); + }; + auto dismiss = [loop](const Appointment&){ + g_message("You clicked 'dismiss'"); + g_main_loop_quit(loop); + }; + + Snap snap; + snap(a, show, dismiss); + g_main_loop_run(loop); + return 0; +} diff --git a/tests/planner-mock.h b/tests/planner-mock.h index 44d30c7..53109cf 100644 --- a/tests/planner-mock.h +++ b/tests/planner-mock.h @@ -20,22 +20,34 @@ #ifndef INDICATOR_DATETIME_PLANNER_MOCK_H #define INDICATOR_DATETIME_PLANNER_MOCK_H -#include <datetime/planner.h> +#include <datetime/planner-range.h> namespace unity { namespace indicator { namespace datetime { /** - * \brief Planner which does nothing on its own. - * It requires its client must set its appointments property. + * \brief #RangePlanner which does nothing on its own. + * Its controller must set its appointments property. */ -class MockPlanner: public Planner +class MockRangePlanner: public RangePlanner { public: - MockPlanner() =default; - virtual ~MockPlanner() =default; + MockRangePlanner(): + m_range(std::pair<DateTime,DateTime>(DateTime::NowLocal(), DateTime::NowLocal())) + { + } + + ~MockRangePlanner() =default; + + core::Property<std::vector<Appointment>>& appointments() { return m_appointments; } + core::Property<std::pair<DateTime,DateTime>>& range() { return m_range; } + +private: + core::Property<std::vector<Appointment>> m_appointments; + core::Property<std::pair<DateTime,DateTime>> m_range; }; + } // namespace datetime } // namespace indicator diff --git a/tests/state-mock.h b/tests/state-mock.h index 721b82f..792c60d 100644 --- a/tests/state-mock.h +++ b/tests/state-mock.h @@ -28,15 +28,21 @@ class MockState: public State { public: std::shared_ptr<MockClock> mock_clock; + std::shared_ptr<MockRangePlanner> mock_range_planner; MockState() { const DateTime now = DateTime::NowLocal(); mock_clock.reset(new MockClock(now)); - settings.reset(new Settings); clock = std::dynamic_pointer_cast<Clock>(mock_clock); - planner.reset(new MockPlanner); - planner->time = now; + + settings.reset(new Settings); + + mock_range_planner.reset(new MockRangePlanner); + auto range_planner = std::dynamic_pointer_cast<RangePlanner>(mock_range_planner); + calendar_month.reset(new MonthPlanner(range_planner, now)); + calendar_upcoming.reset(new UpcomingPlanner(range_planner, now)); + locations.reset(new Locations); } }; diff --git a/tests/test-actions.cpp b/tests/test-actions.cpp index 1865cfd..5d1efd5 100644 --- a/tests/test-actions.cpp +++ b/tests/test-actions.cpp @@ -150,10 +150,10 @@ TEST_F(ActionsFixture, SetCalendarDate) // confirm that Planner.time gets changed to that date when we // activate the 'calendar' action with that date's time_t as the arg - EXPECT_NE (now, m_state->planner->time.get()); + EXPECT_NE (now, m_state->calendar_month->month().get()); auto v = g_variant_new_int64(now.to_unix()); g_action_group_activate_action (action_group, action_name, v); - EXPECT_EQ (now, m_state->planner->time.get()); + EXPECT_EQ (now, m_state->calendar_month->month().get()); } TEST_F(ActionsFixture, ActivatingTheCalendarResetsItsDate) @@ -171,11 +171,12 @@ TEST_F(ActionsFixture, ActivatingTheCalendarResetsItsDate) const auto now = m_state->clock->localtime(); auto next_week = g_date_time_add_weeks(now.get(), 1); const auto next_week_unix = g_date_time_to_unix(next_week); + g_date_time_unref(next_week); g_action_group_activate_action (action_group, "calendar", g_variant_new_int64(next_week_unix)); // confirm the planner and calendar action state moved a week into the future // but that m_state->clock is unchanged - EXPECT_EQ(next_week_unix, m_state->planner->time.get().to_unix()); + EXPECT_EQ(next_week_unix, m_state->calendar_month->month().get().to_unix()); EXPECT_EQ(now, m_state->clock->localtime()); auto calendar_state = g_action_group_get_action_state(action_group, "calendar"); EXPECT_TRUE(calendar_state != nullptr); @@ -196,7 +197,7 @@ TEST_F(ActionsFixture, ActivatingTheCalendarResetsItsDate) g_action_group_change_action_state(action_group, "calendar-active", g_variant_new_boolean(true)); // confirm the planner and calendar action state were reset back to m_state->clock's time - EXPECT_EQ(now.to_unix(), m_state->planner->time.get().to_unix()); + EXPECT_EQ(now.to_unix(), m_state->calendar_month->month().get().to_unix()); EXPECT_EQ(now, m_state->clock->localtime()); calendar_state = g_action_group_get_action_state(action_group, "calendar"); EXPECT_TRUE(calendar_state != nullptr); @@ -215,7 +216,8 @@ TEST_F(ActionsFixture, OpenAppointment) Appointment appt; appt.uid = "some arbitrary uid"; appt.url = "http://www.canonical.com/"; - m_state->planner->upcoming.set(std::vector<Appointment>({appt})); + appt.begin = m_state->clock->localtime(); + m_state->calendar_upcoming->appointments().set(std::vector<Appointment>({appt})); const auto action_name = "activate-appointment"; auto action_group = m_actions->action_group(); diff --git a/tests/test-clock-watcher.cpp b/tests/test-clock-watcher.cpp new file mode 100644 index 0000000..2425fe8 --- /dev/null +++ b/tests/test-clock-watcher.cpp @@ -0,0 +1,172 @@ +/* + * Copyright 2014 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/clock-watcher.h> + +#include <gtest/gtest.h> + +#include "state-fixture.h" + +using namespace unity::indicator::datetime; + +class ClockWatcherFixture: public StateFixture +{ +private: + + typedef StateFixture super; + +protected: + + std::vector<std::string> m_triggered; + std::unique_ptr<ClockWatcher> m_watcher; + std::shared_ptr<RangePlanner> m_range_planner; + std::shared_ptr<UpcomingPlanner> m_upcoming; + + void SetUp() + { + super::SetUp(); + + m_range_planner.reset(new MockRangePlanner); + m_upcoming.reset(new UpcomingPlanner(m_range_planner, m_state->clock->localtime())); + m_watcher.reset(new ClockWatcherImpl(m_state->clock, m_upcoming)); + m_watcher->alarm_reached().connect([this](const Appointment& appt){ + m_triggered.push_back(appt.uid); + }); + + EXPECT_TRUE(m_triggered.empty()); + } + + void TearDown() + { + m_triggered.clear(); + m_watcher.reset(); + m_upcoming.reset(); + m_range_planner.reset(); + + super::TearDown(); + } + + std::vector<Appointment> build_some_appointments() + { + const auto now = m_state->clock->localtime(); + auto tomorrow = g_date_time_add_days (now.get(), 1); + auto tomorrow_begin = g_date_time_add_full (tomorrow, 0, 0, 0, + -g_date_time_get_hour(tomorrow), + -g_date_time_get_minute(tomorrow), + -g_date_time_get_seconds(tomorrow)); + auto tomorrow_end = g_date_time_add_full (tomorrow_begin, 0, 0, 1, 0, 0, -1); + + Appointment a1; // an alarm clock appointment + a1.color = "red"; + a1.summary = "Alarm"; + a1.summary = "http://www.example.com/"; + a1.uid = "example"; + a1.has_alarms = true; + a1.begin = tomorrow_begin; + a1.end = tomorrow_end; + + auto ubermorgen_begin = g_date_time_add_days (tomorrow, 1); + auto ubermorgen_end = g_date_time_add_full (tomorrow_begin, 0, 0, 1, 0, 0, -1); + + Appointment a2; // a non-alarm appointment + a2.color = "green"; + a2.summary = "Other Text"; + a2.summary = "http://www.monkey.com/"; + a2.uid = "monkey"; + a2.has_alarms = false; + a2.begin = ubermorgen_begin; + a2.end = ubermorgen_end; + + // cleanup + g_date_time_unref(ubermorgen_end); + g_date_time_unref(ubermorgen_begin); + g_date_time_unref(tomorrow_end); + g_date_time_unref(tomorrow_begin); + g_date_time_unref(tomorrow); + + return std::vector<Appointment>({a1, a2}); + } +}; + +/*** +**** +***/ + +TEST_F(ClockWatcherFixture, AppointmentsChanged) +{ + // Add some appointments to the planner. + // One of these matches our state's localtime, so that should get triggered. + std::vector<Appointment> a = build_some_appointments(); + a[0].begin = m_state->clock->localtime(); + m_range_planner->appointments().set(a); + + // Confirm that it got fired + EXPECT_EQ(1, m_triggered.size()); + EXPECT_EQ(a[0].uid, m_triggered[0]); +} + + +TEST_F(ClockWatcherFixture, TimeChanged) +{ + // Add some appointments to the planner. + // Neither of these match the state's localtime, so nothing should be triggered. + std::vector<Appointment> a = build_some_appointments(); + m_range_planner->appointments().set(a); + EXPECT_TRUE(m_triggered.empty()); + + // Set the state's clock to a time that matches one of the appointments(). + // That appointment should get triggered. + m_mock_state->mock_clock->set_localtime(a[1].begin); + EXPECT_EQ(1, m_triggered.size()); + EXPECT_EQ(a[1].uid, m_triggered[0]); +} + + +TEST_F(ClockWatcherFixture, MoreThanOne) +{ + const auto now = m_state->clock->localtime(); + std::vector<Appointment> a = build_some_appointments(); + a[0].begin = a[1].begin = now; + m_range_planner->appointments().set(a); + + EXPECT_EQ(2, m_triggered.size()); + EXPECT_EQ(a[0].uid, m_triggered[0]); + EXPECT_EQ(a[1].uid, m_triggered[1]); +} + + +TEST_F(ClockWatcherFixture, NoDuplicates) +{ + // Setup: add an appointment that gets triggered. + const auto now = m_state->clock->localtime(); + const std::vector<Appointment> appointments = build_some_appointments(); + std::vector<Appointment> a; + a.push_back(appointments[0]); + a[0].begin = now; + m_range_planner->appointments().set(a); + EXPECT_EQ(1, m_triggered.size()); + EXPECT_EQ(a[0].uid, m_triggered[0]); + + // Now change the appointment vector by adding one to it. + // Confirm that the ClockWatcher doesn't re-trigger a[0] + a.push_back(appointments[1]); + m_range_planner->appointments().set(a); + EXPECT_EQ(1, m_triggered.size()); + EXPECT_EQ(a[0].uid, m_triggered[0]); +} diff --git a/tests/test-clock.cpp b/tests/test-clock.cpp index 4287e1c..a4924b3 100644 --- a/tests/test-clock.cpp +++ b/tests/test-clock.cpp @@ -37,12 +37,12 @@ class ClockFixture: public TestDBusFixture void emitPrepareForSleep() { g_dbus_connection_emit_signal(g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, nullptr), - NULL, + nullptr, "/org/freedesktop/login1", // object path "org.freedesktop.login1.Manager", // interface "PrepareForSleep", // signal name g_variant_new("(b)", FALSE), - NULL); + nullptr); } }; diff --git a/tests/test-live-actions.cpp b/tests/test-live-actions.cpp index eab8596..d6ef424 100644 --- a/tests/test-live-actions.cpp +++ b/tests/test-live-actions.cpp @@ -284,7 +284,9 @@ TEST_F(LiveActionsFixture, OpenPlannerAt) { const auto now = DateTime::NowLocal(); m_actions->open_planner_at(now); - const std::string expected = now.format("evolution \"calendar:///?startdate=%Y%m%d\""); + const auto today_begins = now.add_full(0, 0, 0, -now.hour(), -now.minute(), -now.seconds()); + const auto gmt = today_begins.to_timezone("UTC"); + const auto expected = gmt.format("evolution \"calendar:///?startdate=%Y%m%dT%H%M%SZ\""); EXPECT_EQ(expected, m_live_actions->last_cmd); } @@ -295,7 +297,8 @@ TEST_F(LiveActionsFixture, CalendarState) const DateTime now (tmp); g_date_time_unref (tmp); m_mock_state->mock_clock->set_localtime (now); - m_state->planner->time.set(now); + m_state->calendar_month->month().set(now); + //m_state->planner->time.set(now); /// /// Test the default calendar state. @@ -315,7 +318,7 @@ TEST_F(LiveActionsFixture, CalendarState) // calendar-day should be in sync with m_state->calendar_day v = g_variant_lookup_value (calendar_state, "calendar-day", G_VARIANT_TYPE_INT64); EXPECT_TRUE (v != nullptr); - EXPECT_EQ (m_state->planner->time.get().to_unix(), g_variant_get_int64(v)); + EXPECT_EQ (m_state->calendar_month->month().get().to_unix(), g_variant_get_int64(v)); g_clear_pointer (&v, g_variant_unref); // show-week-numbers should be false because MockSettings defaults everything to 0 @@ -356,7 +359,7 @@ TEST_F(LiveActionsFixture, CalendarState) a2.begin = next_begin; a2.end = next_end; - m_state->planner->this_month.set(std::vector<Appointment>({a1, a2})); + m_state->calendar_month->appointments().set(std::vector<Appointment>({a1, a2})); /// /// Now test the calendar state again. diff --git a/tests/test-menus.cpp b/tests/test-menus.cpp index 73d6036..29d86b3 100644 --- a/tests/test-menus.cpp +++ b/tests/test-menus.cpp @@ -252,15 +252,17 @@ private: void InspectAppointmentMenuItems(GMenuModel* section, int first_appt_index, - const std::vector<Appointment>& appointments) + const std::vector<Appointment>& appointments, + bool can_open_planner) { // try adding a few appointments and see if the menu updates itself - m_state->planner->upcoming.set(appointments); + m_state->calendar_upcoming->appointments().set(appointments); wait_msec(); // wait a moment for the menu to update //auto submenu = g_menu_model_get_item_link(menu_model, 0, G_MENU_LINK_SUBMENU); //auto section = g_menu_model_get_item_link(submenu, Menu::Appointments, G_MENU_LINK_SECTION); - EXPECT_EQ(appointments.size()+1, g_menu_model_get_n_items(section)); + const int n_add_event_buttons = can_open_planner ? 1 : 0; + EXPECT_EQ(n_add_event_buttons + appointments.size(), g_menu_model_get_n_items(section)); for (int i=0, n=appointments.size(); i<n; i++) InspectAppointmentMenuItem(section, first_appt_index+i, appointments[i]); @@ -269,8 +271,10 @@ private: //g_clear_object(&submenu); } - void InspectDesktopAppointments(GMenuModel* menu_model) + void InspectDesktopAppointments(GMenuModel* menu_model, bool can_open_planner) { + const int n_add_event_buttons = can_open_planner ? 1 : 0; + // get the Appointments section auto submenu = g_menu_model_get_item_link(menu_model, 0, G_MENU_LINK_SUBMENU); @@ -281,42 +285,45 @@ private: EXPECT_EQ(0, g_menu_model_get_n_items(section)); g_clear_object(§ion); - // when "show_events" is true, - // there should be an "add event" button even if there aren't any appointments std::vector<Appointment> appointments; m_state->settings->show_events.set(true); - m_state->planner->upcoming.set(appointments); + m_state->calendar_upcoming->appointments().set(appointments); wait_msec(); section = g_menu_model_get_item_link(submenu, Menu::Appointments, G_MENU_LINK_SECTION); - EXPECT_EQ(1, g_menu_model_get_n_items(section)); - gchar* action = nullptr; - EXPECT_TRUE(g_menu_model_get_item_attribute(section, 0, G_MENU_ATTRIBUTE_ACTION, "s", &action)); - const char* expected_action = "activate-planner"; - EXPECT_EQ(std::string("indicator.")+expected_action, action); - EXPECT_TRUE(g_action_group_has_action(m_actions->action_group(), expected_action)); - g_free(action); + EXPECT_EQ(n_add_event_buttons, g_menu_model_get_n_items(section)); + if (can_open_planner) + { + // when "show_events" is true, + // there should be an "add event" button even if there aren't any appointments + gchar* action = nullptr; + EXPECT_TRUE(g_menu_model_get_item_attribute(section, 0, G_MENU_ATTRIBUTE_ACTION, "s", &action)); + const char* expected_action = "activate-planner"; + EXPECT_EQ(std::string("indicator.")+expected_action, action); + EXPECT_TRUE(g_action_group_has_action(m_actions->action_group(), expected_action)); + g_free(action); + } g_clear_object(§ion); // try adding a few appointments and see if the menu updates itself appointments = build_some_appointments(); - m_state->planner->upcoming.set(appointments); + m_state->calendar_upcoming->appointments().set(appointments); wait_msec(); // wait a moment for the menu to update section = g_menu_model_get_item_link(submenu, Menu::Appointments, G_MENU_LINK_SECTION); - EXPECT_EQ(3, g_menu_model_get_n_items(section)); - InspectAppointmentMenuItems(section, 0, appointments); + EXPECT_EQ(n_add_event_buttons + 2, g_menu_model_get_n_items(section)); + InspectAppointmentMenuItems(section, 0, appointments, can_open_planner); g_clear_object(§ion); // cleanup g_clear_object(&submenu); } - void InspectPhoneAppointments(GMenuModel* menu_model) + void InspectPhoneAppointments(GMenuModel* menu_model, bool can_open_planner) { auto submenu = g_menu_model_get_item_link(menu_model, 0, G_MENU_LINK_SUBMENU); // clear all the appointments std::vector<Appointment> appointments; - m_state->planner->upcoming.set(appointments); + m_state->calendar_upcoming->appointments().set(appointments); wait_msec(); // wait a moment for the menu to update // check that there's a "clock app" menuitem even when there are no appointments @@ -332,11 +339,11 @@ private: // add some appointments and test them appointments = build_some_appointments(); - m_state->planner->upcoming.set(appointments); + m_state->calendar_upcoming->appointments().set(appointments); wait_msec(); // wait a moment for the menu to update section = g_menu_model_get_item_link(submenu, Menu::Appointments, G_MENU_LINK_SECTION); EXPECT_EQ(3, g_menu_model_get_n_items(section)); - InspectAppointmentMenuItems(section, 1, appointments); + InspectAppointmentMenuItems(section, 1, appointments, can_open_planner); g_clear_object(§ion); // cleanup @@ -347,10 +354,12 @@ protected: void InspectAppointments(GMenuModel* menu_model, Menu::Profile profile) { + const auto can_open_planner = m_actions->can_open_planner(); + switch (profile) { case Menu::Desktop: - InspectDesktopAppointments(menu_model); + InspectDesktopAppointments(menu_model, can_open_planner); break; case Menu::DesktopGreeter: @@ -358,7 +367,7 @@ protected: break; case Menu::Phone: - InspectPhoneAppointments(menu_model); + InspectPhoneAppointments(menu_model, can_open_planner); break; case Menu::PhoneGreeter: @@ -507,6 +516,13 @@ TEST_F(MenuFixture, Appointments) { for(auto& menu : m_menus) InspectAppointments(menu->menu_model(), menu->profile()); + + // toggle can_open_planner() and test the desktop again + // to confirm that the "Add Event…" menuitem appears iff + // there's a calendar available user-agent + m_mock_actions->set_can_open_planner (!m_actions->can_open_planner()); + std::shared_ptr<Menu> menu = m_menu_factory->buildMenu(Menu::Desktop); + InspectAppointments(menu->menu_model(), menu->profile()); } TEST_F(MenuFixture, Locations) diff --git a/tests/test-planner.cpp b/tests/test-planner.cpp index b476ee8..8f1590c 100644 --- a/tests/test-planner.cpp +++ b/tests/test-planner.cpp @@ -18,19 +18,18 @@ */ #include "glib-fixture.h" +#include "timezone-mock.h" #include <datetime/appointment.h> #include <datetime/clock-mock.h> #include <datetime/date-time.h> #include <datetime/planner.h> -#include <datetime/planner-eds.h> +#include <datetime/planner-range.h> #include <langinfo.h> #include <locale.h> -using unity::indicator::datetime::Appointment; -using unity::indicator::datetime::DateTime; -using unity::indicator::datetime::PlannerEds; +using namespace unity::indicator::datetime; /*** **** @@ -38,22 +37,6 @@ using unity::indicator::datetime::PlannerEds; typedef GlibFixture PlannerFixture; -TEST_F(PlannerFixture, EDS) -{ - PlannerEds planner; - wait_msec(100); - - auto now = g_date_time_new_now_local(); - planner.time.set(DateTime(now)); - wait_msec(2500); - - std::vector<Appointment> this_month = planner.this_month.get(); - std::cerr << this_month.size() << " appointments this month" << std::endl; - for(const auto& a : this_month) - std::cerr << a.summary << std::endl; -} - - TEST_F(PlannerFixture, HelloWorld) { auto halloween = g_date_time_new_local(2020, 10, 31, 18, 30, 59); diff --git a/tests/test-settings.cpp b/tests/test-settings.cpp index 980e7fa..707247d 100644 --- a/tests/test-settings.cpp +++ b/tests/test-settings.cpp @@ -167,8 +167,8 @@ TEST_F(SettingsFixture, Locations) { const auto key = SETTINGS_LOCATIONS_S; - const gchar* astrv[] = {"America/Los_Angeles Oakland", "America/Chicago Oklahoma City", "Europe/London London", NULL}; - const gchar* bstrv[] = {"America/Denver", "Europe/London London", "Europe/Berlin Berlin", NULL}; + const gchar* astrv[] = {"America/Los_Angeles Oakland", "America/Chicago Oklahoma City", "Europe/London London", nullptr}; + const gchar* bstrv[] = {"America/Denver", "Europe/London London", "Europe/Berlin Berlin", nullptr}; const std::vector<std::string> av = strv_to_vector(astrv); const std::vector<std::string> bv = strv_to_vector(bstrv); diff --git a/tests/test-utils.cpp b/tests/test-utils.cpp index 036c13f..97f07ed 100644 --- a/tests/test-utils.cpp +++ b/tests/test-utils.cpp @@ -59,7 +59,7 @@ namespace const char* location; const char* expected_name; } beautify_timezone_test_cases[] = { - { "America/Chicago", NULL, "Chicago" }, + { "America/Chicago", nullptr, "Chicago" }, { "America/Chicago", "America/Chicago", "Chicago" }, { "America/Chicago", "America/Chigago Chicago", "Chicago" }, { "America/Chicago", "America/Chicago Oklahoma City", "Oklahoma City" }, diff --git a/include/datetime/planner-eds.h b/tests/timezone-mock.h index f3abce0..67584cb 100644 --- a/include/datetime/planner-eds.h +++ b/tests/timezone-mock.h @@ -1,5 +1,5 @@ /* - * Copyright 2013 Canonical Ltd. + * Copyright 2014 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 @@ -17,33 +17,24 @@ * Charles Kerr <charles.kerr@canonical.com> */ -#ifndef INDICATOR_DATETIME_PLANNER_EDS_H -#define INDICATOR_DATETIME_PLANNER_EDS_H +#ifndef INDICATOR_DATETIME_TIMEZONE_MOCK_H +#define INDICATOR_DATETIME_TIMEZONE_MOCK_H -#include <datetime/planner.h> - -#include <memory> // unique_ptr +#include <datetime/timezone.h> namespace unity { namespace indicator { namespace datetime { -/** - * \brief Planner which uses EDS as its backend - */ -class PlannerEds: public Planner +class MockTimezone: public Timezone { public: - PlannerEds(); - virtual ~PlannerEds(); - -private: - class Impl; - std::unique_ptr<Impl> p; + MockTimezone() =default; + ~MockTimezone() =default; }; } // namespace datetime } // namespace indicator } // namespace unity -#endif // INDICATOR_DATETIME_PLANNER_EDS_H +#endif // INDICATOR_DATETIME_TIMEZONE_MOCK_H |