/* * 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) { // preload the alarm icon from click m_serialized_alarm_icon = create_alarm_icon(); // 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_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() { 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; iclock->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: 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 MenuFactory::buildMenu(Menu::Profile profile) { std::shared_ptr menu; switch (profile) { case Menu::Desktop: menu.reset(new DesktopMenu(m_state, m_actions)); break; case Menu::DesktopGreeter: menu.reset(new DesktopGreeterMenu(m_state, m_actions)); break; case Menu::Phone: menu.reset(new PhoneMenu(m_state, m_actions)); break; case Menu::PhoneGreeter: menu.reset(new PhoneGreeterMenu(m_state, m_actions)); break; default: g_warn_if_reached(); break; } return menu; } /**** ***** ****/ } // namespace datetime } // namespace indicator } // namespace unity