/*
* Copyright 2013 Canonical Ltd.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3, as published
* by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranties of
* MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see .
*
* Authors:
* Charles Kerr
*/
#include
#include
#include
#include
#include
#include
namespace unity {
namespace indicator {
namespace datetime {
/****
*****
****/
#define FALLBACK_ALARM_CLOCK_ICON_NAME "clock"
#define CALENDAR_ICON_NAME "calendar"
class MenuImpl: public Menu
{
protected:
MenuImpl(const Menu::Profile profile_in,
const std::string& name_in,
std::shared_ptr& state,
std::shared_ptr& actions,
std::shared_ptr formatter):
Menu(profile_in, name_in),
m_state(state),
m_actions(actions),
m_formatter(formatter)
{
// initialize the menu
create_gmenu();
for (int i=0; iheader.changed().connect([this](const std::string&){
update_header();
});
m_formatter->headerFormat.changed().connect([this](const std::string&){
update_section(Locations); // need to update x-canonical-time-format
});
m_formatter->relativeFormatChanged.connect([this](){
update_section(Appointments); // uses formatter.getRelativeFormat()
update_section(Locations); // uses formatter.getRelativeFormat()
});
m_state->settings->show_clock.changed().connect([this](bool){
update_header(); // update header's label
update_section(Locations); // locations' relative time may have changed
});
m_state->settings->show_calendar.changed().connect([this](bool){
update_section(Calendar);
});
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&){
update_section(Appointments); // "upcoming" is the list of Appointments we show
});
m_state->clock->dateChanged.connect([this](){
update_section(Calendar); // need to update the Date menuitem
update_section(Locations); // locations' relative time may have changed
});
m_state->locations->locations.changed().connect([this](const std::vector&) {
update_section(Locations); // "locations" is the list of Locations we show
});
}
virtual ~MenuImpl()
{
g_clear_object(&m_menu);
g_clear_pointer(&m_serialized_alarm_icon, g_variant_unref);
g_clear_pointer(&m_serialized_calendar_icon, g_variant_unref);
}
virtual GVariant* create_header_state() =0;
void update_header()
{
auto action_group = m_actions->action_group();
auto action_name = name() + "-header";
auto state = create_header_state();
g_action_group_change_action_state(action_group, action_name.c_str(), state);
}
std::shared_ptr m_state;
std::shared_ptr m_actions;
std::shared_ptr m_formatter;
GMenu* m_submenu = nullptr;
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);
g_object_unref(i);
}
return serialized;
}
GVariant* get_serialized_calendar_icon()
{
if (G_UNLIKELY(m_serialized_calendar_icon == nullptr))
{
auto i = g_themed_icon_new_with_default_fallbacks(CALENDAR_ICON_NAME);
m_serialized_calendar_icon = g_icon_serialize(i);
g_object_unref(i);
}
return m_serialized_calendar_icon;
}
void create_gmenu()
{
g_assert(m_submenu == nullptr);
m_submenu = g_menu_new();
// build placeholders for the sections
for(int i=0; isettings->show_calendar.get() &&
((profile == Desktop) || (profile == DesktopGreeter));
auto menu = g_menu_new();
// add a menuitem that shows the current date
auto label = m_state->clock->localtime().format(_("%A, %e %B %Y"));
auto item = g_menu_item_new (label.c_str(), nullptr);
auto v = get_serialized_calendar_icon();
g_menu_item_set_attribute_value (item, G_MENU_ATTRIBUTE_ICON, v);
if (allow_activation)
{
v = g_variant_new_int64(0);
const char* action = "indicator.activate-planner";
g_menu_item_set_action_and_target_value (item, action, v);
}
g_menu_append_item(menu, item);
g_object_unref(item);
// add calendar
if (show_calendar)
{
item = g_menu_item_new ("[calendar]", NULL);
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",
"s", "com.canonical.indicator.calendar");
if (allow_activation)
{
g_menu_item_set_attribute (item, "activation-action",
"s", "indicator.activate-planner");
}
g_menu_append_item (menu, item);
g_object_unref (item);
}
return G_MENU_MODEL(menu);
}
void add_appointments(GMenu* menu, Profile profile)
{
int n = 0;
const int MAX_APPTS = 5;
std::set added;
for (const auto& appt : m_state->planner->upcoming.get())
{
if (n++ >= MAX_APPTS)
break;
if (added.count(appt.uid))
continue;
added.insert(appt.uid);
GDateTime* begin = appt.begin();
GDateTime* end = appt.end();
auto fmt = m_formatter->getRelativeFormat(begin, end);
auto unix_time = g_date_time_to_unix(begin);
auto menu_item = g_menu_item_new (appt.summary.c_str(), nullptr);
g_menu_item_set_attribute (menu_item, "x-canonical-time", "x", unix_time);
g_menu_item_set_attribute (menu_item, "x-canonical-time-format", "s", fmt.c_str());
if (appt.has_alarms)
{
g_menu_item_set_attribute (menu_item, "x-canonical-type", "s", "com.canonical.indicator.alarm");
g_menu_item_set_attribute_value(menu_item, G_MENU_ATTRIBUTE_ICON, get_serialized_alarm_icon());
}
else
{
g_menu_item_set_attribute (menu_item, "x-canonical-type", "s", "com.canonical.indicator.appointment");
if (!appt.color.empty())
g_menu_item_set_attribute (menu_item, "x-canonical-color", "s", appt.color.c_str());
}
if (profile == Phone)
g_menu_item_set_action_and_target_value (menu_item,
"indicator.activate-appointment",
g_variant_new_string (appt.uid.c_str()));
else
g_menu_item_set_action_and_target_value (menu_item,
"indicator.activate-planner",
g_variant_new_int64 (unix_time));
g_menu_append_item (menu, menu_item);
g_object_unref (menu_item);
}
}
GMenuModel* create_appointments_section(Profile profile)
{
auto menu = g_menu_new();
if (((profile==Phone) || (profile==Desktop)) && m_state->settings->show_events.get())
{
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);
}
return G_MENU_MODEL(menu);
}
GMenuModel* create_locations_section(Profile profile)
{
GMenu* menu = g_menu_new();
if (profile == Desktop)
{
const auto now = m_state->clock->localtime();
for(const auto& location : m_state->locations->locations.get())
{
const auto& zone = location.zone();
const auto& name = location.name();
const auto zone_now = now.to_timezone(zone);
const auto fmt = m_formatter->getRelativeFormat(zone_now.get());
auto detailed_action = g_strdup_printf("indicator.set-location::%s %s", zone.c_str(), name.c_str());
auto i = g_menu_item_new (name.c_str(), detailed_action);
g_menu_item_set_attribute(i, "x-canonical-type", "s", "com.canonical.indicator.location");
g_menu_item_set_attribute(i, "x-canonical-timezone", "s", zone.c_str());
g_menu_item_set_attribute(i, "x-canonical-time-format", "s", fmt.c_str());
g_menu_append_item (menu, i);
g_object_unref(i);
g_free(detailed_action);
}
}
return G_MENU_MODEL(menu);
}
GMenuModel* create_settings_section(Profile profile)
{
auto menu = g_menu_new();
if (profile == Desktop)
{
g_menu_append (menu, _("Date & Time Settings…"), "indicator.activate-desktop-settings");
}
else if (profile == Phone)
{
g_menu_append (menu, _("Time & Date settings…"), "indicator.activate-phone-settings");
}
return G_MENU_MODEL (menu);
}
void update_section(Section section)
{
GMenuModel * model;
const auto p = profile();
switch (section)
{
case Calendar: model = create_calendar_section(p); break;
case Appointments: model = create_appointments_section(p); break;
case Locations: model = create_locations_section(p); break;
case Settings: model = create_settings_section(p); break;
default: model = nullptr; g_warn_if_reached();
}
if (model)
{
g_menu_remove(m_submenu, section);
g_menu_insert_section(m_submenu, section, nullptr, model);
g_object_unref(model);
}
}
//private:
GVariant * m_serialized_alarm_icon = nullptr;
GVariant * m_serialized_calendar_icon = nullptr;
}; // class MenuImpl
/***
****
***/
class DesktopBaseMenu: public MenuImpl
{
protected:
DesktopBaseMenu(Menu::Profile profile_,
const std::string& name_,
std::shared_ptr& state_,
std::shared_ptr& actions_):
MenuImpl(profile_, name_, state_, actions_,
std::shared_ptr(new DesktopFormatter(state_->clock, state_->settings)))
{
update_header();
}
GVariant* create_header_state()
{
const auto visible = m_state->settings->show_clock.get();
const auto title = _("Date and Time");
auto label = g_variant_new_string(m_formatter->header.get().c_str());
GVariantBuilder b;
g_variant_builder_init(&b, G_VARIANT_TYPE_VARDICT);
g_variant_builder_add(&b, "{sv}", "accessible-desc", label);
g_variant_builder_add(&b, "{sv}", "label", label);
g_variant_builder_add(&b, "{sv}", "title", g_variant_new_string(title));
g_variant_builder_add(&b, "{sv}", "visible", g_variant_new_boolean(visible));
return g_variant_builder_end(&b);
}
};
class DesktopMenu: public DesktopBaseMenu
{
public:
DesktopMenu(std::shared_ptr& state_, std::shared_ptr& actions_):
DesktopBaseMenu(Desktop,"desktop", state_, actions_) {}
};
class DesktopGreeterMenu: public DesktopBaseMenu
{
public:
DesktopGreeterMenu(std::shared_ptr& state_, std::shared_ptr& actions_):
DesktopBaseMenu(DesktopGreeter,"desktop_greeter", state_, actions_) {}
};
class PhoneBaseMenu: public MenuImpl
{
protected:
PhoneBaseMenu(Menu::Profile profile_,
const std::string& name_,
std::shared_ptr& state_,
std::shared_ptr& actions_):
MenuImpl(profile_, name_, state_, actions_,
std::shared_ptr(new PhoneFormatter(state_->clock)))
{
update_header();
}
GVariant* create_header_state()
{
// are there alarms?
bool has_alarms = false;
for(const auto& appointment : m_state->planner->upcoming.get())
if((has_alarms = appointment.has_alarms))
break;
GVariantBuilder b;
g_variant_builder_init(&b, G_VARIANT_TYPE_VARDICT);
g_variant_builder_add(&b, "{sv}", "title", g_variant_new_string (_("Upcoming")));
g_variant_builder_add(&b, "{sv}", "visible", g_variant_new_boolean (TRUE));
if (has_alarms)
{
auto label = m_formatter->header.get();
auto a11y = g_strdup_printf(_("%s (has alarms)"), label.c_str());
g_variant_builder_add(&b, "{sv}", "label", g_variant_new_string(label.c_str()));
g_variant_builder_add(&b, "{sv}", "accessible-desc", g_variant_new_take_string(a11y));
g_variant_builder_add(&b, "{sv}", "icon", get_serialized_alarm_icon());
}
else
{
auto v = g_variant_new_string(m_formatter->header.get().c_str());
g_variant_builder_add(&b, "{sv}", "label", v);
g_variant_builder_add(&b, "{sv}", "accessible-desc", v);
}
return g_variant_builder_end (&b);
}
};
class PhoneMenu: public PhoneBaseMenu
{
public:
PhoneMenu(std::shared_ptr& state_,
std::shared_ptr& actions_):
PhoneBaseMenu(Phone, "phone", state_, actions_) {}
};
class PhoneGreeterMenu: public PhoneBaseMenu
{
public:
PhoneGreeterMenu(std::shared_ptr& state_,
std::shared_ptr& actions_):
PhoneBaseMenu(PhoneGreeter, "phone_greeter", state_, actions_) {}
};
/****
*****
****/
MenuFactory::MenuFactory(std::shared_ptr& actions_,
std::shared_ptr& state_):
m_actions(actions_),
m_state(state_)
{
}
std::shared_ptr