diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/CMakeLists.txt | 7 | ||||
-rw-r--r-- | src/actions-live.cpp | 32 | ||||
-rw-r--r-- | src/actions.cpp | 13 | ||||
-rw-r--r-- | src/clock-watcher.cpp | 81 | ||||
-rw-r--r-- | src/date-time.cpp | 57 | ||||
-rw-r--r-- | src/engine-eds.cpp (renamed from src/planner-eds.cpp) | 466 | ||||
-rw-r--r-- | src/main.cpp | 48 | ||||
-rw-r--r-- | src/menu.cpp | 137 | ||||
-rw-r--r-- | src/planner-month.cpp | 66 | ||||
-rw-r--r-- | src/planner-range.cpp | 105 | ||||
-rw-r--r-- | src/planner-upcoming.cpp | 61 | ||||
-rw-r--r-- | src/snap.cpp | 328 | ||||
-rw-r--r-- | src/timezone-file.cpp | 27 | ||||
-rw-r--r-- | src/utils.c | 4 |
14 files changed, 1141 insertions, 291 deletions
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); |