/*
* 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
extern "C"
{
#include
}
namespace ayatana {
namespace indicator {
namespace datetime {
/****
*****
****/
Menu::Menu (Profile profile_in, const std::string& name_in):
m_profile(profile_in),
m_name(name_in)
{
}
const std::string& Menu::name() const
{
return m_name;
}
Menu::Profile Menu::profile() const
{
return m_profile;
}
GMenuModel* Menu::menu_model()
{
return G_MENU_MODEL(m_menu);
}
/****
*****
****/
#define ALARM_ICON_NAME "alarm-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)
{
if (m_pSettings == NULL && profile_in != Phone && profile_in != PhoneGreeter)
{
m_pSettings = g_settings_new("org.ayatana.common");
g_signal_connect(m_pSettings, "changed::max-menu-text-length", G_CALLBACK(on_string_length_changed), this);
}
// initialize the menu
create_gmenu();
for (int i=0; iheader.changed().connect([this](const std::string&){
update_header();
});
m_formatter->header_format.changed().connect([this](const std::string&){
update_section(Locations); // need to update x-ayatana-time-format
});
m_formatter->relative_format_changed.connect([this](){
update_section(Appointments); // uses formatter.relative_format()
update_section(Locations); // uses formatter.relative_format()
});
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->calendar_upcoming->date().changed().connect([this](const DateTime&){
update_upcoming(); // our m_upcoming is planner->upcoming() filtered by time
});
m_state->calendar_upcoming->appointments().changed().connect([this](const std::vector&){
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&) {
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);
g_clear_object(&m_pSettings);
}
virtual GVariant* create_header_state() =0;
static void on_string_length_changed(GSettings *pSettings, gchar *sKey, gpointer pSelf)
{
static_cast(pSelf)->update_section(Appointments);
}
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);
}
void update_upcoming()
{
// The usual case is on desktop (and /only/ case on phone)
// is that we're looking at the current date and want to see
// "the next five calendar events, if any."
//
// However on the Desktop when the user clicks onto a different
// calendar date, show the next five calendar events starting
// from the beginning of that clicked day.
DateTime begin;
const auto now = m_state->clock->localtime();
const auto calendar_day = m_state->calendar_month->month().get();
if ((profile() == Desktop) && !DateTime::is_same_day(now, calendar_day))
begin = calendar_day.start_of_day();
else
begin = now.start_of_minute();
std::vector upcoming;
for(const auto& a : m_state->calendar_upcoming->appointments().get())
if (begin <= 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 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))
{
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 m_serialized_alarm_icon;
}
std::vector m_upcoming;
private:
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) || (profile == Phone));
auto menu = g_menu_new();
const char * action_name;
if (profile == Phone)
action_name = "indicator.phone.open-calendar-app";
else if (profile == Desktop)
action_name = "indicator.desktop.open-calendar-app";
else
action_name = nullptr;
/* Translators, please edit/rearrange these strftime(3) tokens to suit your locale!
Format string for the day on the first menuitem in the datetime indicator.
This format string gives the full weekday, date, month, and year.
en_US example: "%A, %B %e %Y" --> Saturday, October 31 2020"
en_GB example: "%A, %e %B %Y" --> Saturday, 31 October 2020" */
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 (action_name != nullptr)
{
v = g_variant_new_int64(0);
g_menu_item_set_action_and_target_value (item, action_name, v);
}
g_menu_append_item(menu, item);
g_object_unref(item);
// add calendar
if (show_calendar)
{
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-ayatana-type",
"s", "org.ayatana.indicator.calendar");
if (action_name != nullptr)
g_menu_item_set_attribute (item, "activation-action", "s", action_name);
g_menu_append_item (menu, item);
g_object_unref (item);
}
return G_MENU_MODEL(menu);
}
void add_appointments(GMenu* menu, Profile profile)
{
const int MAX_APPTS = 5;
std::set added;
const char * action_name;
if (profile == Phone)
action_name = "indicator.phone.open-appointment";
else if ((profile == Desktop) && m_actions->desktop_has_calendar_app())
action_name = "indicator.desktop.open-appointment";
else
action_name = nullptr;
for (const auto& appt : m_upcoming)
{
// 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();
GDateTime* end = appt.end();
auto fmt = m_formatter->relative_format(begin, end);
auto unix_time = g_date_time_to_unix(begin);
ayatana_common_utils_ellipsize((char*)appt.summary.c_str());
auto menu_item = g_menu_item_new (appt.summary.c_str(), nullptr);
g_menu_item_set_attribute (menu_item, "x-ayatana-time", "x", unix_time);
g_menu_item_set_attribute (menu_item, "x-ayatana-time-format", "s", fmt.c_str());
if (appt.is_ubuntu_alarm())
{
g_menu_item_set_attribute (menu_item, "x-ayatana-type", "s", "org.ayatana.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-ayatana-type", "s", "org.ayatana.indicator.appointment");
}
if (!appt.color.empty())
g_menu_item_set_attribute (menu_item, "x-ayatana-color", "s", appt.color.c_str());
if (action_name != nullptr)
g_menu_item_set_action_and_target_value (menu_item, action_name,
g_variant_new_string (appt.uid.c_str()));
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==Desktop) && m_state->settings->show_events.get())
{
add_appointments (menu, profile);
if (m_actions->desktop_has_calendar_app())
{
// add the 'Add Event…' menuitem
auto menu_item = g_menu_item_new(_("Add Event…"), nullptr);
const gchar* action_name = "indicator.desktop.open-calendar-app";
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)
{
auto menu_item = g_menu_item_new (_("Clock"), "indicator.phone.open-alarm-app");
g_menu_item_set_attribute_value (menu_item, G_MENU_ATTRIBUTE_ICON, get_serialized_alarm_icon());
g_menu_append_item (menu, menu_item);
g_object_unref (menu_item);
add_appointments (menu, profile);
}
return G_MENU_MODEL(menu);
}
GMenuModel* create_locations_section(Profile profile)
{
GMenu* menu = g_menu_new();
const auto now = m_state->clock->localtime();
if (profile == Desktop || profile == Phone)
{
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->relative_format(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-ayatana-type", "s", "org.ayatana.indicator.location");
g_menu_item_set_attribute(i, "x-ayatana-timezone", "s", zone.c_str());
g_menu_item_set_attribute(i, "x-ayatana-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();
const char * action_name;
if (profile == Desktop)
action_name = "indicator.desktop.open-settings-app";
else if (profile == Phone)
action_name = "indicator.phone.open-settings-app";
else
action_name = nullptr;
if (action_name != nullptr)
g_menu_append (menu, _("Time and Date Settings…"), action_name);
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;
GSettings *m_pSettings = NULL;
}; // 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 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(TRUE));
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_ubuntu_alarms = false;
for(const auto& appointment : m_upcoming)
if((has_ubuntu_alarms = appointment.is_ubuntu_alarm()))
break;
GVariantBuilder b;
g_variant_builder_init(&b, G_VARIANT_TYPE_VARDICT);
g_variant_builder_add(&b, "{sv}", "title", g_variant_new_string (_("Time and Date")));
g_variant_builder_add(&b, "{sv}", "visible", g_variant_new_boolean (TRUE));
if (has_ubuntu_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(const std::shared_ptr& actions_,
const std::shared_ptr& state_):
m_actions(actions_),
m_state(state_)
{
}
std::shared_ptr