aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt7
-rw-r--r--src/actions-live.cpp32
-rw-r--r--src/actions.cpp13
-rw-r--r--src/clock-watcher.cpp81
-rw-r--r--src/date-time.cpp57
-rw-r--r--src/engine-eds.cpp (renamed from src/planner-eds.cpp)466
-rw-r--r--src/main.cpp48
-rw-r--r--src/menu.cpp137
-rw-r--r--src/planner-month.cpp66
-rw-r--r--src/planner-range.cpp105
-rw-r--r--src/planner-upcoming.cpp61
-rw-r--r--src/snap.cpp328
-rw-r--r--src/timezone-file.cpp27
-rw-r--r--src/utils.c4
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);