diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/CMakeLists.txt | 21 | ||||
-rw-r--r-- | src/gmenuharness/CMakeLists.txt | 17 | ||||
-rw-r--r-- | src/gmenuharness/MatchResult.cpp | 187 | ||||
-rw-r--r-- | src/gmenuharness/MatchUtils.cpp | 77 | ||||
-rw-r--r-- | src/gmenuharness/MenuItemMatcher.cpp | 1008 | ||||
-rw-r--r-- | src/gmenuharness/MenuMatcher.cpp | 208 | ||||
-rw-r--r-- | src/service.vala | 407 | ||||
-rw-r--r-- | src/sound-menu.vala | 38 | ||||
-rw-r--r-- | src/volume-control-pulse.vala | 83 | ||||
-rw-r--r-- | src/volume-control.vala | 14 |
10 files changed, 1985 insertions, 75 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 194dfc9..6c621ae 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,12 +8,12 @@ set(SYMBOLS_PATH "${CMAKE_CURRENT_BINARY_DIR}/indicator-sound-service.def") set(VAPI_PATH "${CMAKE_CURRENT_BINARY_DIR}/indicator-sound-service.vapi") vapi_gen(accounts-service - LIBRARY - accounts-service - PACKAGES - gio-2.0 - INPUT - /usr/share/gir-1.0/AccountsService-1.0.gir + LIBRARY + accounts-service + PACKAGES + gio-2.0 + INPUT + /usr/share/gir-1.0/AccountsService-1.0.gir ) vala_init(indicator-sound-service @@ -70,7 +70,7 @@ vala_add(indicator-sound-service media-player-user.vala DEPENDS media-player - accounts-service-sound-settings + accounts-service-sound-settings greeter-broadcast ) vala_add(indicator-sound-service @@ -103,6 +103,7 @@ vala_add(indicator-sound-service sound-menu.vala DEPENDS media-player + volume-control ) vala_add(indicator-sound-service accounts-service-user.vala @@ -164,8 +165,8 @@ add_definitions( ) add_library( - indicator-sound-service-lib STATIC - ${INDICATOR_SOUND_SOURCES} + indicator-sound-service-lib STATIC + ${INDICATOR_SOUND_SOURCES} ) target_link_libraries( @@ -206,3 +207,5 @@ install( RUNTIME DESTINATION ${CMAKE_INSTALL_LIBEXECDIR}/indicator-sound/ ) +# Disable integration tests +# add_subdirectory(gmenuharness) diff --git a/src/gmenuharness/CMakeLists.txt b/src/gmenuharness/CMakeLists.txt new file mode 100644 index 0000000..c9e613a --- /dev/null +++ b/src/gmenuharness/CMakeLists.txt @@ -0,0 +1,17 @@ +pkg_check_modules(UNITY_API libunity-api>=0.1.3 REQUIRED) +include_directories(${UNITY_API_INCLUDE_DIRS}) + +include_directories("${CMAKE_SOURCE_DIR}/include") + +add_library( + gmenuharness-shared SHARED + MatchResult.cpp + MatchUtils.cpp + MenuItemMatcher.cpp + MenuMatcher.cpp +) + +target_link_libraries( + gmenuharness-shared + ${GLIB_LDFLAGS} +) diff --git a/src/gmenuharness/MatchResult.cpp b/src/gmenuharness/MatchResult.cpp new file mode 100644 index 0000000..40629aa --- /dev/null +++ b/src/gmenuharness/MatchResult.cpp @@ -0,0 +1,187 @@ +/* + * Copyright © 2014 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser 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 warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authored by: Pete Woods <pete.woods@canonical.com> + */ + +#include <unity/gmenuharness/MatchResult.h> + +#include <chrono> +#include <map> +#include <sstream> +#include <iostream> + +using namespace std; + +namespace unity +{ + +namespace gmenuharness +{ + +namespace +{ + + +static void printLocation(ostream& ss, const vector<unsigned int>& location, bool first) +{ + for (int i : location) + { + ss << " "; + if (first) + { + ss << i; + } + else + { + ss << " "; + } + } + ss << " "; +} + +struct compare_vector +{ + bool operator()(const vector<unsigned int>& a, + const vector<unsigned int>& b) const + { + auto p1 = a.begin(); + auto p2 = b.begin(); + + while (p1 != a.end()) + { + if (p2 == b.end()) + { + return false; + } + if (*p2 > *p1) + { + return true; + } + if (*p1 > *p2) + { + return false; + } + + ++p1; + ++p2; + } + + if (p2 != b.end()) + { + return true; + } + + return false; + } +}; +} + +struct MatchResult::Priv +{ + bool m_success = true; + + map<vector<unsigned int>, vector<string>, compare_vector> m_failures; + + chrono::time_point<chrono::system_clock> m_timeout = chrono::system_clock::now() + chrono::seconds(10); +}; + +MatchResult::MatchResult() : + p(new Priv) +{ +} + +MatchResult::MatchResult(MatchResult&& other) +{ + *this = move(other); +} + +MatchResult::MatchResult(const MatchResult& other) : + p(new Priv) +{ + *this = other; +} + +MatchResult& MatchResult::operator=(const MatchResult& other) +{ + p->m_success = other.p->m_success; + p->m_failures= other.p->m_failures; + return *this; +} + +MatchResult& MatchResult::operator=(MatchResult&& other) +{ + p = move(other.p); + return *this; +} + +MatchResult MatchResult::createChild() const +{ + MatchResult child; + child.p->m_timeout = p->m_timeout; + return child; +} + +void MatchResult::failure(const vector<unsigned int>& location, const string& message) +{ + p->m_success = false; + auto it = p->m_failures.find(location); + if (it == p->m_failures.end()) + { + it = p->m_failures.insert(make_pair(location, vector<string>())).first; + } + it->second.emplace_back(message); +} + +void MatchResult::merge(const MatchResult& other) +{ + p->m_success &= other.p->m_success; + for (const auto& e : other.p->m_failures) + { + p->m_failures.insert(make_pair(e.first, e.second)); + } +} + +bool MatchResult::success() const +{ + return p->m_success; +} + +bool MatchResult::hasTimedOut() const +{ + auto now = chrono::system_clock::now(); + return (now >= p->m_timeout); +} + +string MatchResult::concat_failures() const +{ + stringstream ss; + ss << "Failed expectations:" << endl; + for (const auto& failure : p->m_failures) + { + bool first = true; + for (const string& s: failure.second) + { + printLocation(ss, failure.first, first); + first = false; + ss << s << endl; + } + } + return ss.str(); +} + +} // namespace gmenuharness + +} // namespace unity diff --git a/src/gmenuharness/MatchUtils.cpp b/src/gmenuharness/MatchUtils.cpp new file mode 100644 index 0000000..7b87a25 --- /dev/null +++ b/src/gmenuharness/MatchUtils.cpp @@ -0,0 +1,77 @@ +/* + * Copyright © 2014 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser 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 warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authored by: Pete Woods <pete.woods@canonical.com> + */ + +#include <unity/gmenuharness/MatchUtils.h> + +#include <unity/util/ResourcePtr.h> + +using namespace std; +namespace util = unity::util; + +namespace unity +{ + +namespace gmenuharness +{ + +void waitForCore (GObject * obj, const string& signalName, unsigned int timeout) { + shared_ptr<GMainLoop> loop(g_main_loop_new(nullptr, false), &g_main_loop_unref); + + /* Our two exit criteria */ + util::ResourcePtr<gulong, function<void(gulong)>> signal( + g_signal_connect_swapped(obj, signalName.c_str(), + G_CALLBACK(g_main_loop_quit), loop.get()), + [obj](gulong s) + { + g_signal_handler_disconnect(obj, s); + }); + + util::ResourcePtr<guint, function<void(guint)>> timer(g_timeout_add(timeout, + [](gpointer user_data) -> gboolean + { + g_main_loop_quit((GMainLoop *)user_data); + return G_SOURCE_CONTINUE; + }, + loop.get()), + &g_source_remove); + + /* Wait for sync */ + g_main_loop_run(loop.get()); +} + +void menuWaitForItems(const shared_ptr<GMenuModel>& menu, unsigned int timeout) +{ + waitForCore(G_OBJECT(menu.get()), "items-changed", timeout); +} + +void g_object_deleter(gpointer object) +{ + g_clear_object(&object); +} + +void gvariant_deleter(GVariant* varptr) +{ + if (varptr != nullptr) + { + g_variant_unref(varptr); + } +} + +} // namespace gmenuharness + +} // namespace unity diff --git a/src/gmenuharness/MenuItemMatcher.cpp b/src/gmenuharness/MenuItemMatcher.cpp new file mode 100644 index 0000000..f39acef --- /dev/null +++ b/src/gmenuharness/MenuItemMatcher.cpp @@ -0,0 +1,1008 @@ +/* + * Copyright © 2014 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser 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 warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authored by: Pete Woods <pete.woods@canonical.com> + */ + +#include <unity/gmenuharness/MatchResult.h> +#include <unity/gmenuharness/MatchUtils.h> +#include <unity/gmenuharness/MenuItemMatcher.h> + +#include <iostream> +#include <vector> +#include <map> + +using namespace std; + +namespace unity +{ + +namespace gmenuharness +{ + +namespace +{ + +enum class LinkType +{ + any, + section, + submenu +}; + +static string bool_to_string(bool value) +{ + return value? "true" : "false"; +} + +static shared_ptr<GVariant> get_action_group_attribute(const shared_ptr<GActionGroup>& actionGroup, const gchar* attribute) +{ + shared_ptr<GVariant> value( + g_action_group_get_action_state(actionGroup.get(), attribute), + &gvariant_deleter); + return value; +} + +static shared_ptr<GVariant> get_attribute(const shared_ptr<GMenuItem> menuItem, const gchar* attribute) +{ + shared_ptr<GVariant> value( + g_menu_item_get_attribute_value(menuItem.get(), attribute, nullptr), + &gvariant_deleter); + return value; +} + +static string get_string_attribute(const shared_ptr<GMenuItem> menuItem, const gchar* attribute) +{ + string result; + char* temp = nullptr; + if (g_menu_item_get_attribute(menuItem.get(), attribute, "s", &temp)) + { + result = temp; + g_free(temp); + } + return result; +} + +static pair<string, string> split_action(const string& action) +{ + auto index = action.find('.'); + + if (index == string::npos) + { + return make_pair(string(), action); + } + + return make_pair(action.substr(0, index), action.substr(index + 1, action.size())); +} + +static string type_to_string(MenuItemMatcher::Type type) +{ + switch(type) + { + case MenuItemMatcher::Type::plain: + return "plain"; + case MenuItemMatcher::Type::checkbox: + return "checkbox"; + case MenuItemMatcher::Type::radio: + return "radio"; + } + + return string(); +} +} + +struct MenuItemMatcher::Priv +{ + void all(MatchResult& matchResult, const vector<unsigned int>& location, + const shared_ptr<GMenuModel>& menu, + map<string, shared_ptr<GActionGroup>>& actions) + { + int count = g_menu_model_get_n_items(menu.get()); + + if (m_items.size() != (unsigned int) count) + { + matchResult.failure( + location, + "Expected " + to_string(m_items.size()) + + " children, but found " + to_string(count)); + return; + } + + for (size_t i = 0; i < m_items.size(); ++i) + { + const auto& matcher = m_items.at(i); + matcher.match(matchResult, location, menu, actions, i); + } + } + + void startsWith(MatchResult& matchResult, const vector<unsigned int>& location, + const shared_ptr<GMenuModel>& menu, + map<string, shared_ptr<GActionGroup>>& actions) + { + int count = g_menu_model_get_n_items(menu.get()); + if (m_items.size() > (unsigned int) count) + { + matchResult.failure( + location, + "Expected at least " + to_string(m_items.size()) + + " children, but found " + to_string(count)); + return; + } + + for (size_t i = 0; i < m_items.size(); ++i) + { + const auto& matcher = m_items.at(i); + matcher.match(matchResult, location, menu, actions, i); + } + } + + void endsWith(MatchResult& matchResult, const vector<unsigned int>& location, + const shared_ptr<GMenuModel>& menu, + map<string, shared_ptr<GActionGroup>>& actions) + { + int count = g_menu_model_get_n_items(menu.get()); + if (m_items.size() > (unsigned int) count) + { + matchResult.failure( + location, + "Expected at least " + to_string(m_items.size()) + + " children, but found " + to_string(count)); + return; + } + + // match the last N items + size_t j; + for (size_t i = count - m_items.size(), j = 0; i < count && j < m_items.size(); ++i, ++j) + { + const auto& matcher = m_items.at(j); + matcher.match(matchResult, location, menu, actions, i); + } + } + + Type m_type = Type::plain; + + Mode m_mode = Mode::all; + + LinkType m_linkType = LinkType::any; + + shared_ptr<size_t> m_expectedSize; + + shared_ptr<string> m_label; + + shared_ptr<string> m_icon; + + map<shared_ptr<string>, vector<std::string>> m_themed_icons; + + shared_ptr<string> m_action; + + vector<std::string> m_state_icons; + + vector<pair<string, shared_ptr<GVariant>>> m_attributes; + + vector<string> m_not_exist_attributes; + + vector<pair<string, shared_ptr<GVariant>>> m_pass_through_attributes; + + shared_ptr<bool> m_isToggled; + + vector<MenuItemMatcher> m_items; + + vector<pair<string, shared_ptr<GVariant>>> m_activations; + + vector<pair<string, shared_ptr<GVariant>>> m_setActionStates; + + double m_maxDifference = 0.0; +}; + +MenuItemMatcher MenuItemMatcher::checkbox() +{ + MenuItemMatcher matcher; + matcher.type(Type::checkbox); + return matcher; +} + +MenuItemMatcher MenuItemMatcher::radio() +{ + MenuItemMatcher matcher; + matcher.type(Type::radio); + return matcher; +} + +MenuItemMatcher::MenuItemMatcher() : + p(new Priv) +{ +} + +MenuItemMatcher::~MenuItemMatcher() +{ +} + +MenuItemMatcher::MenuItemMatcher(const MenuItemMatcher& other) : + p(new Priv) +{ + *this = other; +} + +MenuItemMatcher::MenuItemMatcher(MenuItemMatcher&& other) +{ + *this = move(other); +} + +MenuItemMatcher& MenuItemMatcher::operator=(const MenuItemMatcher& other) +{ + p->m_type = other.p->m_type; + p->m_mode = other.p->m_mode; + p->m_expectedSize = other.p->m_expectedSize; + p->m_label = other.p->m_label; + p->m_icon = other.p->m_icon; + p->m_themed_icons = other.p->m_themed_icons; + p->m_action = other.p->m_action; + p->m_state_icons = other.p->m_state_icons; + p->m_attributes = other.p->m_attributes; + p->m_not_exist_attributes = other.p->m_not_exist_attributes; + p->m_pass_through_attributes = other.p->m_pass_through_attributes; + p->m_isToggled = other.p->m_isToggled; + p->m_linkType = other.p->m_linkType; + p->m_items = other.p->m_items; + p->m_activations = other.p->m_activations; + p->m_setActionStates = other.p->m_setActionStates; + p->m_maxDifference = other.p->m_maxDifference; + return *this; +} + +MenuItemMatcher& MenuItemMatcher::operator=(MenuItemMatcher&& other) +{ + p = move(other.p); + return *this; +} + +MenuItemMatcher& MenuItemMatcher::type(Type type) +{ + p->m_type = type; + return *this; +} + +MenuItemMatcher& MenuItemMatcher::label(const string& label) +{ + p->m_label = make_shared<string>(label); + return *this; +} + +MenuItemMatcher& MenuItemMatcher::action(const string& action) +{ + p->m_action = make_shared<string>(action); + return *this; +} + +MenuItemMatcher& MenuItemMatcher::state_icons(const std::vector<std::string>& state_icons) +{ + p->m_state_icons = state_icons; + return *this; +} + +MenuItemMatcher& MenuItemMatcher::icon(const string& icon) +{ + p->m_icon = make_shared<string>(icon); + return *this; +} + +MenuItemMatcher& MenuItemMatcher::themed_icon(const std::string& iconName, const std::vector<std::string>& icons) +{ + p->m_themed_icons[make_shared<string>(iconName)] = icons; + return *this; +} + +MenuItemMatcher& MenuItemMatcher::widget(const string& widget) +{ + return string_attribute("x-canonical-type", widget); +} + +MenuItemMatcher& MenuItemMatcher::pass_through_attribute(const string& actionName, const shared_ptr<GVariant>& value) +{ + p->m_pass_through_attributes.emplace_back(actionName, value); + return *this; +} + +MenuItemMatcher& MenuItemMatcher::pass_through_boolean_attribute(const string& actionName, bool value) +{ + return pass_through_attribute( + actionName, + shared_ptr<GVariant>(g_variant_new_boolean(value), + &gvariant_deleter)); +} + +MenuItemMatcher& MenuItemMatcher::pass_through_string_attribute(const string& actionName, const string& value) +{ + return pass_through_attribute( + actionName, + shared_ptr<GVariant>(g_variant_new_string(value.c_str()), + &gvariant_deleter)); +} + +MenuItemMatcher& MenuItemMatcher::pass_through_double_attribute(const std::string& actionName, double value) +{ + return pass_through_attribute( + actionName, + shared_ptr<GVariant>(g_variant_new_double(value), + &gvariant_deleter)); +} + +MenuItemMatcher& MenuItemMatcher::round_doubles(double maxDifference) +{ + p->m_maxDifference = maxDifference; + return *this; +} + +MenuItemMatcher& MenuItemMatcher::attribute(const string& name, const shared_ptr<GVariant>& value) +{ + p->m_attributes.emplace_back(name, value); + return *this; +} + +MenuItemMatcher& MenuItemMatcher::boolean_attribute(const string& name, bool value) +{ + return attribute( + name, + shared_ptr<GVariant>(g_variant_new_boolean(value), + &gvariant_deleter)); +} + +MenuItemMatcher& MenuItemMatcher::string_attribute(const string& name, const string& value) +{ + return attribute( + name, + shared_ptr<GVariant>(g_variant_new_string(value.c_str()), + &gvariant_deleter)); +} + +MenuItemMatcher& MenuItemMatcher::int32_attribute(const std::string& name, int value) +{ + return attribute( + name, + shared_ptr<GVariant>(g_variant_new_int32 (value), + &gvariant_deleter)); +} + +MenuItemMatcher& MenuItemMatcher::int64_attribute(const std::string& name, int value) +{ + return attribute( + name, + shared_ptr<GVariant>(g_variant_new_int64 (value), + &gvariant_deleter)); +} + +MenuItemMatcher& MenuItemMatcher::double_attribute(const std::string& name, double value) +{ + return attribute( + name, + shared_ptr<GVariant>(g_variant_new_double (value), + &gvariant_deleter)); +} + +MenuItemMatcher& MenuItemMatcher::attribute_not_set(const std::string& name) +{ + p->m_not_exist_attributes.emplace_back (name); + return *this; +} + +MenuItemMatcher& MenuItemMatcher::toggled(bool isToggled) +{ + p->m_isToggled = make_shared<bool>(isToggled); + return *this; +} + +MenuItemMatcher& MenuItemMatcher::mode(Mode mode) +{ + p->m_mode = mode; + return *this; +} + +MenuItemMatcher& MenuItemMatcher::submenu() +{ + p->m_linkType = LinkType::submenu; + return *this; +} + +MenuItemMatcher& MenuItemMatcher::section() +{ + p->m_linkType = LinkType::section; + return *this; +} + +MenuItemMatcher& MenuItemMatcher::is_empty() +{ + return has_exactly(0); +} + +MenuItemMatcher& MenuItemMatcher::has_exactly(size_t children) +{ + p->m_expectedSize = make_shared<size_t>(children); + return *this; +} + +MenuItemMatcher& MenuItemMatcher::item(const MenuItemMatcher& item) +{ + p->m_items.emplace_back(item); + return *this; +} + +MenuItemMatcher& MenuItemMatcher::item(MenuItemMatcher&& item) +{ + p->m_items.emplace_back(item); + return *this; +} + +MenuItemMatcher& MenuItemMatcher::pass_through_activate(std::string const& action, const shared_ptr<GVariant>& parameter) +{ + p->m_activations.emplace_back(action, parameter); + return *this; +} + +MenuItemMatcher& MenuItemMatcher::activate(const shared_ptr<GVariant>& parameter) +{ + p->m_activations.emplace_back(string(), parameter); + return *this; +} + +MenuItemMatcher& MenuItemMatcher::set_pass_through_action_state(const std::string& action, const std::shared_ptr<GVariant>& state) +{ + p->m_setActionStates.emplace_back(action, state); + return *this; +} + +MenuItemMatcher& MenuItemMatcher::set_action_state(const std::shared_ptr<GVariant>& state) +{ + p->m_setActionStates.emplace_back("", state); + return *this; +} + +void MenuItemMatcher::match( + MatchResult& matchResult, + const vector<unsigned int>& parentLocation, + const shared_ptr<GMenuModel>& menu, + map<string, shared_ptr<GActionGroup>>& actions, + int index) const +{ + shared_ptr<GMenuItem> menuItem(g_menu_item_new_from_model(menu.get(), index), &g_object_deleter); + + vector<unsigned int> location(parentLocation); + location.emplace_back(index); + + string action = get_string_attribute(menuItem, G_MENU_ATTRIBUTE_ACTION); + + bool isCheckbox = false; + bool isRadio = false; + bool isToggled = false; + + pair<string, string> idPair; + shared_ptr<GActionGroup> actionGroup; + shared_ptr<GVariant> state; + + if (!action.empty()) + { + idPair = split_action(action); + actionGroup = actions[idPair.first]; + state = shared_ptr<GVariant>(g_action_group_get_action_state(actionGroup.get(), + idPair.second.c_str()), + &gvariant_deleter); + auto attributeTarget = get_attribute(menuItem, G_MENU_ATTRIBUTE_TARGET); + + if (attributeTarget && state) + { + isToggled = g_variant_equal(state.get(), attributeTarget.get()); + isRadio = true; + } + else if (state + && g_variant_is_of_type(state.get(), G_VARIANT_TYPE_BOOLEAN)) + { + isToggled = g_variant_get_boolean(state.get()); + isCheckbox = true; + } + } + + Type actualType = Type::plain; + if (isCheckbox) + { + actualType = Type::checkbox; + } + else if (isRadio) + { + actualType = Type::radio; + } + + if (actualType != p->m_type) + { + matchResult.failure( + location, + "Expected " + type_to_string(p->m_type) + ", found " + + type_to_string(actualType)); + } + + // check themed icons + map<shared_ptr<string>, vector<string>>::iterator iter; + for (iter = p->m_themed_icons.begin(); iter != p->m_themed_icons.end(); ++iter) + { + auto icon_val = g_menu_item_get_attribute_value(menuItem.get(), (*iter).first->c_str(), nullptr); + if (!icon_val) + { + matchResult.failure( + location, + "Expected themed icon " + (*(*iter).first) + " was not found"); + } + + auto gicon = g_icon_deserialize(icon_val); + if (!gicon || !G_IS_THEMED_ICON(gicon)) + { + matchResult.failure( + location, + "Expected attribute " + (*(*iter).first) + " is not a themed icon"); + } + auto iconNames = g_themed_icon_get_names(G_THEMED_ICON(gicon)); + int nb_icons = 0; + while(iconNames[nb_icons]) + { + ++nb_icons; + } + + if (nb_icons != (*iter).second.size()) + { + matchResult.failure( + location, + "Expected " + to_string((*iter).second.size()) + + " icons for themed icon [" + (*(*iter).first) + + "], but " + to_string(nb_icons) + " were found."); + } + else + { + // now compare all the icons + for (int i = 0; i < nb_icons; ++i) + { + if (string(iconNames[i]) != (*iter).second[i]) + { + matchResult.failure( + location, + "Icon at position " + to_string(i) + + " for themed icon [" + (*(*iter).first) + + "], mismatchs. Expected: " + iconNames[i] + " but found " + (*iter).second[i]); + } + } + } + g_object_unref(gicon); + } + + string label = get_string_attribute(menuItem, G_MENU_ATTRIBUTE_LABEL); + if (p->m_label && (*p->m_label) != label) + { + matchResult.failure( + location, + "Expected label '" + *p->m_label + "', but found '" + label + + "'"); + } + + string icon = get_string_attribute(menuItem, G_MENU_ATTRIBUTE_ICON); + if (p->m_icon && (*p->m_icon) != icon) + { + matchResult.failure( + location, + "Expected icon '" + *p->m_icon + "', but found '" + icon + "'"); + } + + if (p->m_action && (*p->m_action) != action) + { + matchResult.failure( + location, + "Expected action '" + *p->m_action + "', but found '" + action + + "'"); + } + + if (!p->m_state_icons.empty() && !state) + { + matchResult.failure( + location, + "Expected state icons but no state was found"); + } + else if (!p->m_state_icons.empty() && state && + !g_variant_is_of_type(state.get(), G_VARIANT_TYPE_VARDICT)) + { + matchResult.failure( + location, + "Expected state icons vardict, found " + + type_to_string(actualType)); + } + else if (!p->m_state_icons.empty() && state && + g_variant_is_of_type(state.get(), G_VARIANT_TYPE_VARDICT)) + { + std::vector<std::string> actual_state_icons; + GVariantIter it; + gchar* key; + GVariant* value; + + g_variant_iter_init(&it, state.get()); + while (g_variant_iter_loop(&it, "{sv}", &key, &value)) + { + if (std::string(key) == "icon") { + auto gicon = g_icon_deserialize(value); + if (gicon && G_IS_THEMED_ICON(gicon)) + { + auto iconNames = g_themed_icon_get_names(G_THEMED_ICON(gicon)); + // Just take the first icon in the list (there is only ever one) + actual_state_icons.push_back(iconNames[0]); + g_object_unref(gicon); + } + } + else if (std::string(key) == "icons" && g_variant_is_of_type(value, G_VARIANT_TYPE("av"))) + { + // If we find "icons" in the map, clear any icons we may have found in "icon", + // then break from the loop as we have found all icons now. + actual_state_icons.clear(); + GVariantIter icon_it; + GVariant* icon_value; + + g_variant_iter_init(&icon_it, value); + while (g_variant_iter_loop(&icon_it, "v", &icon_value)) + { + auto gicon = g_icon_deserialize(icon_value); + if (gicon && G_IS_THEMED_ICON(gicon)) + { + auto iconNames = g_themed_icon_get_names(G_THEMED_ICON(gicon)); + // Just take the first icon in the list (there is only ever one) + actual_state_icons.push_back(iconNames[0]); + g_object_unref(gicon); + } + } + // We're breaking out of g_variant_iter_loop here so clean up + g_variant_unref(value); + g_free(key); + break; + } + } + + if (p->m_state_icons != actual_state_icons) + { + std::string expected_icons; + for (unsigned int i = 0; i < p->m_state_icons.size(); ++i) + { + expected_icons += i == 0 ? p->m_state_icons[i] : ", " + p->m_state_icons[i]; + } + std::string actual_icons; + for (unsigned int i = 0; i < actual_state_icons.size(); ++i) + { + actual_icons += i == 0 ? actual_state_icons[i] : ", " + actual_state_icons[i]; + } + matchResult.failure( + location, + "Expected state_icons == {" + expected_icons + + "} but found {" + actual_icons + "}"); + } + } + + for (const auto& e: p->m_pass_through_attributes) + { + string actionName = get_string_attribute(menuItem, e.first.c_str()); + if (actionName.empty()) + { + matchResult.failure( + location, + "Could not find action name '" + e.first + "'"); + } + else + { + auto passThroughIdPair = split_action(actionName); + auto actionGroup = actions[passThroughIdPair.first]; + if (actionGroup) + { + auto value = get_action_group_attribute( + actionGroup, passThroughIdPair.second.c_str()); + if (!value) + { + matchResult.failure( + location, + "Expected pass-through attribute '" + e.first + + "' was not present"); + } + else if (!g_variant_is_of_type(e.second.get(), g_variant_get_type(value.get()))) + { + std::string expectedType = g_variant_get_type_string(e.second.get()); + std::string actualType = g_variant_get_type_string(value.get()); + matchResult.failure( + location, + "Expected pass-through attribute type '" + expectedType + + "' but found '" + actualType + "'"); + } + else if (g_variant_compare(e.second.get(), value.get())) + { + bool reportMismatch = true; + if (g_strcmp0(g_variant_get_type_string(value.get()),"d") == 0 && p->m_maxDifference) + { + auto actualDouble = g_variant_get_double(value.get()); + auto expectedDouble = g_variant_get_double(e.second.get()); + auto difference = actualDouble-expectedDouble; + if (difference < 0) difference = difference * -1.0; + if (difference <= p->m_maxDifference) + { + reportMismatch = false; + } + } + if (reportMismatch) + { + gchar* expectedString = g_variant_print(e.second.get(), true); + gchar* actualString = g_variant_print(value.get(), true); + matchResult.failure( + location, + "Expected pass-through attribute '" + e.first + + "' == " + expectedString + " but found " + + actualString); + + g_free(expectedString); + g_free(actualString); + } + } + } + else + { + matchResult.failure(location, "Could not find action group for ID '" + passThroughIdPair.first + "'"); + } + } + } + + for (const auto& e: p->m_attributes) + { + auto value = get_attribute(menuItem, e.first.c_str()); + if (!value) + { + matchResult.failure(location, + "Expected attribute '" + e.first + + "' could not be found"); + } + else if (!g_variant_is_of_type(e.second.get(), g_variant_get_type(value.get()))) + { + std::string expectedType = g_variant_get_type_string(e.second.get()); + std::string actualType = g_variant_get_type_string(value.get()); + matchResult.failure( + location, + "Expected attribute type '" + expectedType + + "' but found '" + actualType + "'"); + } + else if (g_variant_compare(e.second.get(), value.get())) + { + gchar* expectedString = g_variant_print(e.second.get(), true); + gchar* actualString = g_variant_print(value.get(), true); + matchResult.failure( + location, + "Expected attribute '" + e.first + "' == " + expectedString + + ", but found " + actualString); + g_free(expectedString); + g_free(actualString); + } + } + + for (const auto& e: p->m_not_exist_attributes) + { + auto value = get_attribute(menuItem, e.c_str()); + if (value) + { + matchResult.failure(location, + "Not expected attribute '" + e + + "' was found"); + } + } + + if (p->m_isToggled && (*p->m_isToggled) != isToggled) + { + matchResult.failure( + location, + "Expected toggled = " + bool_to_string(*p->m_isToggled) + + ", but found " + bool_to_string(isToggled)); + } + + if (!matchResult.success()) + { + return; + } + + if (!p->m_items.empty() || p->m_expectedSize) + { + shared_ptr<GMenuModel> link; + + switch (p->m_linkType) + { + case LinkType::any: + { + link.reset(g_menu_model_get_item_link(menu.get(), (int) index, G_MENU_LINK_SUBMENU), &g_object_deleter); + if (!link) + { + link.reset(g_menu_model_get_item_link(menu.get(), (int) index, G_MENU_LINK_SECTION), &g_object_deleter); + } + break; + } + case LinkType::submenu: + { + link.reset(g_menu_model_get_item_link(menu.get(), (int) index, G_MENU_LINK_SUBMENU), &g_object_deleter); + break; + } + case LinkType::section: + { + link.reset(g_menu_model_get_item_link(menu.get(), (int) index, G_MENU_LINK_SECTION), &g_object_deleter); + break; + } + } + + + if (!link) + { + if (p->m_expectedSize) + { + matchResult.failure( + location, + "Expected " + to_string(*p->m_expectedSize) + + " children, but found none"); + } + else + { + matchResult.failure( + location, + "Expected " + to_string(p->m_items.size()) + + " children, but found none"); + } + return; + } + else + { + while (true) + { + MatchResult childMatchResult(matchResult.createChild()); + + if (p->m_expectedSize + && *p->m_expectedSize + != (unsigned int) g_menu_model_get_n_items( + link.get())) + { + childMatchResult.failure( + location, + "Expected " + to_string(*p->m_expectedSize) + + " child items, but found " + + to_string( + g_menu_model_get_n_items( + link.get()))); + } + else if (!p->m_items.empty()) + { + switch (p->m_mode) + { + case Mode::all: + p->all(childMatchResult, location, link, actions); + break; + case Mode::starts_with: + p->startsWith(childMatchResult, location, link, actions); + break; + case Mode::ends_with: + p->endsWith(childMatchResult, location, link, actions); + break; + } + } + + if (childMatchResult.success()) + { + matchResult.merge(childMatchResult); + break; + } + else + { + if (matchResult.hasTimedOut()) + { + matchResult.merge(childMatchResult); + break; + } + menuWaitForItems(link); + } + } + } + } + + + for (const auto& a: p->m_setActionStates) + { + auto stateAction = action; + auto stateIdPair = idPair; + auto stateActionGroup = actionGroup; + if (!a.first.empty()) + { + stateAction = get_string_attribute(menuItem, a.first.c_str());; + stateIdPair = split_action(stateAction); + stateActionGroup = actions[stateIdPair.first]; + } + + if (stateAction.empty()) + { + matchResult.failure( + location, + "Tried to set action state, but no action was found"); + } + else if(!stateActionGroup) + { + matchResult.failure( + location, + "Tried to set action state for action group '" + stateIdPair.first + + "', but action group wasn't found"); + } + else if (!g_action_group_has_action(stateActionGroup.get(), stateIdPair.second.c_str())) + { + matchResult.failure( + location, + "Tried to set action state for action '" + stateAction + + "', but action was not found"); + } + else + { + g_action_group_change_action_state(stateActionGroup.get(), stateIdPair.second.c_str(), + g_variant_ref(a.second.get())); + } + + // FIXME this is a dodgy way to ensure the action state change gets dispatched + menuWaitForItems(menu, 100); + } + + for (const auto& a: p->m_activations) + { + string tmpAction = action; + auto tmpIdPair = idPair; + auto tmpActionGroup = actionGroup; + if (!a.first.empty()) + { + tmpAction = get_string_attribute(menuItem, a.first.c_str()); + tmpIdPair = split_action(tmpAction); + tmpActionGroup = actions[tmpIdPair.first]; + } + + if (tmpAction.empty()) + { + matchResult.failure( + location, + "Tried to activate action, but no action was found"); + } + else if(!tmpActionGroup) + { + matchResult.failure( + location, + "Tried to activate action group '" + tmpIdPair.first + + "', but action group wasn't found"); + } + else if (!g_action_group_has_action(tmpActionGroup.get(), tmpIdPair.second.c_str())) + { + matchResult.failure( + location, + "Tried to activate action '" + tmpAction + "', but action was not found"); + } + else + { + if (a.second) + { + g_action_group_activate_action(tmpActionGroup.get(), tmpIdPair.second.c_str(), + g_variant_ref(a.second.get())); + } + else + { + g_action_group_activate_action(tmpActionGroup.get(), tmpIdPair.second.c_str(), nullptr); + } + + // FIXME this is a dodgy way to ensure the activation gets dispatched + menuWaitForItems(menu, 100); + } + } +} + +} // namepsace gmenuharness + +} // namespace unity diff --git a/src/gmenuharness/MenuMatcher.cpp b/src/gmenuharness/MenuMatcher.cpp new file mode 100644 index 0000000..5bb4fbd --- /dev/null +++ b/src/gmenuharness/MenuMatcher.cpp @@ -0,0 +1,208 @@ +/* + * Copyright © 2014 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser 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 warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authored by: Pete Woods <pete.woods@canonical.com> + */ + +#include <unity/gmenuharness/MenuMatcher.h> +#include <unity/gmenuharness/MatchUtils.h> + +#include <iostream> + +#include <gio/gio.h> + +using namespace std; + +namespace unity +{ + +namespace gmenuharness +{ + +namespace +{ + +static void gdbus_connection_deleter(GDBusConnection* connection) +{ +// if (!g_dbus_connection_is_closed(connection)) +// { +// g_dbus_connection_close_sync(connection, nullptr, nullptr); +// } + g_clear_object(&connection); +} +} + +struct MenuMatcher::Parameters::Priv +{ + string m_busName; + + vector<pair<string, string>> m_actions; + + string m_menuObjectPath; +}; + +MenuMatcher::Parameters::Parameters(const string& busName, + const vector<pair<string, string>>& actions, + const string& menuObjectPath) : + p(new Priv) +{ + p->m_busName = busName; + p->m_actions = actions; + p->m_menuObjectPath = menuObjectPath; +} + +MenuMatcher::Parameters::~Parameters() +{ +} + +MenuMatcher::Parameters::Parameters(const Parameters& other) : + p(new Priv) +{ + *this = other; +} + +MenuMatcher::Parameters::Parameters(Parameters&& other) +{ + *this = move(other); +} + +MenuMatcher::Parameters& MenuMatcher::Parameters::operator=(const Parameters& other) +{ + p->m_busName = other.p->m_busName; + p->m_actions = other.p->m_actions; + p->m_menuObjectPath = other.p->m_menuObjectPath; + return *this; +} + +MenuMatcher::Parameters& MenuMatcher::Parameters::operator=(Parameters&& other) +{ + p = move(other.p); + return *this; +} + +struct MenuMatcher::Priv +{ + Priv(const Parameters& parameters) : + m_parameters(parameters) + { + } + + Parameters m_parameters; + + vector<MenuItemMatcher> m_items; + + shared_ptr<GDBusConnection> m_system; + + shared_ptr<GDBusConnection> m_session; + + shared_ptr<GMenuModel> m_menu; + + map<string, shared_ptr<GActionGroup>> m_actions; +}; + +MenuMatcher::MenuMatcher(const Parameters& parameters) : + p(new Priv(parameters)) +{ + p->m_system.reset(g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, nullptr), + &gdbus_connection_deleter); + g_dbus_connection_set_exit_on_close(p->m_system.get(), false); + + p->m_session.reset(g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr), + &gdbus_connection_deleter); + g_dbus_connection_set_exit_on_close(p->m_session.get(), false); + + p->m_menu.reset( + G_MENU_MODEL( + g_dbus_menu_model_get( + p->m_session.get(), + p->m_parameters.p->m_busName.c_str(), + p->m_parameters.p->m_menuObjectPath.c_str())), + &g_object_deleter); + + for (const auto& action : p->m_parameters.p->m_actions) + { + shared_ptr<GActionGroup> actionGroup( + G_ACTION_GROUP( + g_dbus_action_group_get( + p->m_session.get(), + p->m_parameters.p->m_busName.c_str(), + action.second.c_str())), + &g_object_deleter); + p->m_actions[action.first] = actionGroup; + } +} + +MenuMatcher::~MenuMatcher() +{ +} + +MenuMatcher& MenuMatcher::item(const MenuItemMatcher& item) +{ + p->m_items.emplace_back(item); + return *this; +} + +void MenuMatcher::match(MatchResult& matchResult) const +{ + vector<unsigned int> location; + + while (true) + { + MatchResult childMatchResult(matchResult.createChild()); + + int menuSize = g_menu_model_get_n_items(p->m_menu.get()); + if (p->m_items.size() > (unsigned int) menuSize) + { + childMatchResult.failure( + location, + "Row count mismatch, expected " + to_string(p->m_items.size()) + + " but found " + to_string(menuSize)); + } + else + { + for (size_t i = 0; i < p->m_items.size(); ++i) + { + const auto& matcher = p->m_items.at(i); + matcher.match(childMatchResult, location, p->m_menu, p->m_actions, i); + } + } + + if (childMatchResult.success()) + { + matchResult.merge(childMatchResult); + break; + } + else + { + if (matchResult.hasTimedOut()) + { + matchResult.merge(childMatchResult); + break; + } + menuWaitForItems(p->m_menu); + } + } +} + +MatchResult MenuMatcher::match() const +{ + MatchResult matchResult; + match(matchResult); + return matchResult; +} + +} // namespace gmenuharness + +} // namespace unity diff --git a/src/service.vala b/src/service.vala index a08edf3..12e2ac2 100644 --- a/src/service.vala +++ b/src/service.vala @@ -43,7 +43,7 @@ public class IndicatorSound.Service: Object { warn_notification.closed.connect((n) => { n.clear_actions(); }); BusWatcher.watch_namespace (GLib.BusType.SESSION, "org.freedesktop.Notifications", - () => { debug("Notifications name appeared"); notify_server_caps_checked = false; }, + () => { debug("Notifications name appeared"); }, () => { debug("Notifications name vanshed"); notify_server_caps_checked = false; }); this.settings = new Settings ("com.canonical.indicator.sound"); @@ -52,6 +52,8 @@ public class IndicatorSound.Service: Object { this.notify["visible"].connect ( () => this.update_root_icon () ); this.volume_control = volume; + this.volume_control.active_output_changed.connect (this.update_root_icon); + this.volume_control.active_output_changed.connect (this.update_notification); this.accounts_service = accounts; /* If we're on the greeter, don't export */ @@ -90,6 +92,10 @@ public class IndicatorSound.Service: Object { this.volume_control.bind_property ("high-volume", menu, "show-high-volume-warning", BindingFlags.SYNC_CREATE); }); + this.menus.@foreach ( (profile, menu) => { + this.volume_control.active_output_changed.connect (menu.update_volume_slider); + }); + this.sync_preferred_players (); this.settings.changed["interested-media-players"].connect ( () => { this.sync_preferred_players (); @@ -245,17 +251,7 @@ public class IndicatorSound.Service: Object { void update_root_icon () { double volume = this.volume_control.volume.volume; - string icon; - if (this.volume_control.mute || volume <= 0.0) - icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel"; - else if (this.accounts_service != null && this.accounts_service.silentMode) - icon = "audio-volume-muted-panel"; - else if (volume <= 0.3) - icon = "audio-volume-low-panel"; - else if (volume <= 0.7) - icon = "audio-volume-medium-panel"; - else - icon = "audio-volume-high-panel"; + string icon = get_volume_root_icon (volume, this.volume_control.mute, volume_control.active_output); string accessible_name; if (this.volume_control.mute) { @@ -281,16 +277,334 @@ public class IndicatorSound.Service: Object { private bool notify_server_supports_actions = false; private bool notify_server_supports_sync = false; private bool block_info_notifications = false; + private bool waiting_user_approve_warn = false; - private void update_notification () { + private string get_volume_icon (double volume, VolumeControl.ActiveOutput active_output) + { + string icon = ""; + switch (active_output) + { + case VolumeControl.ActiveOutput.SPEAKERS: + if (volume <= 0.0) + icon = "audio-volume-muted"; + else if (volume <= 0.3) + icon = "audio-volume-low"; + else if (volume <= 0.7) + icon = "audio-volume-medium"; + else + icon = "audio-volume-high"; + break; + case VolumeControl.ActiveOutput.HEADPHONES: + if (volume <= 0.0) + icon = "audio-volume-muted"; + else if (volume <= 0.3) + icon = "audio-volume-low"; + else if (volume <= 0.7) + icon = "audio-volume-medium"; + else + icon = "audio-volume-high"; + break; + case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES: + if (volume <= 0.0) + icon = "audio-volume-muted"; + else if (volume <= 0.3) + icon = "audio-volume-low"; + else if (volume <= 0.7) + icon = "audio-volume-medium"; + else + icon = "audio-volume-high"; + break; + case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER: + if (volume <= 0.0) + icon = "audio-volume-muted"; + else if (volume <= 0.3) + icon = "audio-volume-low"; + else if (volume <= 0.7) + icon = "audio-volume-medium"; + else + icon = "audio-volume-high"; + break; + case VolumeControl.ActiveOutput.USB_SPEAKER: + if (volume <= 0.0) + icon = "audio-volume-muted"; + else if (volume <= 0.3) + icon = "audio-volume-low"; + else if (volume <= 0.7) + icon = "audio-volume-medium"; + else + icon = "audio-volume-high"; + break; + case VolumeControl.ActiveOutput.USB_HEADPHONES: + if (volume <= 0.0) + icon = "audio-volume-muted"; + else if (volume <= 0.3) + icon = "audio-volume-low"; + else if (volume <= 0.7) + icon = "audio-volume-medium"; + else + icon = "audio-volume-high"; + break; + case VolumeControl.ActiveOutput.HDMI_SPEAKER: + if (volume <= 0.0) + icon = "audio-volume-muted"; + else if (volume <= 0.3) + icon = "audio-volume-low"; + else if (volume <= 0.7) + icon = "audio-volume-medium"; + else + icon = "audio-volume-high"; + break; + case VolumeControl.ActiveOutput.HDMI_HEADPHONES: + if (volume <= 0.0) + icon = "audio-volume-muted"; + else if (volume <= 0.3) + icon = "audio-volume-low"; + else if (volume <= 0.7) + icon = "audio-volume-medium"; + else + icon = "audio-volume-high"; + break; + } + return icon; + } + + private string get_volume_root_icon_by_volume (double volume, VolumeControl.ActiveOutput active_output) + { + string icon = ""; + switch (active_output) + { + case VolumeControl.ActiveOutput.SPEAKERS: + if (volume <= 0.0) + icon = "audio-volume-muted-panel"; + else if (volume <= 0.3) + icon = "audio-volume-low-panel"; + else if (volume <= 0.7) + icon = "audio-volume-medium-panel"; + else + icon = "audio-volume-high-panel"; + break; + case VolumeControl.ActiveOutput.HEADPHONES: + if (volume <= 0.0) + icon = "audio-volume-muted-panel"; + else if (volume <= 0.3) + icon = "audio-volume-low-panel"; + else if (volume <= 0.7) + icon = "audio-volume-medium-panel"; + else + icon = "audio-volume-high-panel"; + break; + case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES: + if (volume <= 0.0) + icon = "audio-volume-muted-panel"; + else if (volume <= 0.3) + icon = "audio-volume-low-panel"; + else if (volume <= 0.7) + icon = "audio-volume-medium-panel"; + else + icon = "audio-volume-high-panel"; + break; + case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER: + if (volume <= 0.0) + icon = "audio-volume-muted-panel"; + else if (volume <= 0.3) + icon = "audio-volume-low-panel"; + else if (volume <= 0.7) + icon = "audio-volume-medium-panel"; + else + icon = "audio-volume-high-panel"; + break; + case VolumeControl.ActiveOutput.USB_SPEAKER: + if (volume <= 0.0) + icon = "audio-volume-muted-panel"; + else if (volume <= 0.3) + icon = "audio-volume-low-panel"; + else if (volume <= 0.7) + icon = "audio-volume-medium-panel"; + else + icon = "audio-volume-high-panel"; + break; + case VolumeControl.ActiveOutput.USB_HEADPHONES: + if (volume <= 0.0) + icon = "audio-volume-muted-panel"; + else if (volume <= 0.3) + icon = "audio-volume-low-panel"; + else if (volume <= 0.7) + icon = "audio-volume-medium-panel"; + else + icon = "audio-volume-high-panel"; + break; + case VolumeControl.ActiveOutput.HDMI_SPEAKER: + if (volume <= 0.0) + icon = "audio-volume-muted-panel"; + else if (volume <= 0.3) + icon = "audio-volume-low-panel"; + else if (volume <= 0.7) + icon = "audio-volume-medium-panel"; + else + icon = "audio-volume-high-panel"; + break; + case VolumeControl.ActiveOutput.HDMI_HEADPHONES: + if (volume <= 0.0) + icon = "audio-volume-muted-panel"; + else if (volume <= 0.3) + icon = "audio-volume-low-panel"; + else if (volume <= 0.7) + icon = "audio-volume-medium-panel"; + else + icon = "audio-volume-high-panel"; + break; + } + return icon; + } + + private string get_volume_notification_icon (double volume, bool loud, VolumeControl.ActiveOutput active_output) { + string icon = ""; + if (loud) { + switch (active_output) + { + case VolumeControl.ActiveOutput.SPEAKERS: + icon = "audio-volume-high"; + break; + case VolumeControl.ActiveOutput.HEADPHONES: + icon = "audio-volume-high"; + break; + case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES: + icon = "audio-volume-high"; + break; + case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER: + icon = "audio-volume-high"; + break; + case VolumeControl.ActiveOutput.USB_SPEAKER: + icon = "audio-volume-high"; + break; + case VolumeControl.ActiveOutput.USB_HEADPHONES: + icon = "audio-volume-high"; + break; + case VolumeControl.ActiveOutput.HDMI_SPEAKER: + icon = "audio-volume-high"; + break; + case VolumeControl.ActiveOutput.HDMI_HEADPHONES: + icon = "audio-volume-high"; + break; + } + } else { + icon = get_volume_icon (volume, active_output); + } + return icon; + } + + private string get_volume_root_icon (double volume, bool mute, VolumeControl.ActiveOutput active_output) { + string icon = ""; + switch (active_output) + { + case VolumeControl.ActiveOutput.SPEAKERS: + if (mute || volume <= 0.0) + icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel"; + else if (this.accounts_service != null && this.accounts_service.silentMode) + icon = "audio-volume-muted-panel"; + else + icon = get_volume_root_icon_by_volume (volume, active_output); + break; + case VolumeControl.ActiveOutput.HEADPHONES: + if (mute || volume <= 0.0) + icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel"; + else if (this.accounts_service != null && this.accounts_service.silentMode) + icon = "audio-volume-muted-panel"; + else + icon = get_volume_root_icon_by_volume (volume, active_output); + break; + case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES: + if (mute || volume <= 0.0) + icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel"; + else if (this.accounts_service != null && this.accounts_service.silentMode) + icon = "audio-volume-muted-panel"; + else + icon = get_volume_root_icon_by_volume (volume, active_output); + break; + case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER: + if (mute || volume <= 0.0) + icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel"; + else if (this.accounts_service != null && this.accounts_service.silentMode) + icon = "audio-volume-muted-panel"; + else + icon = get_volume_root_icon_by_volume (volume, active_output); + break; + case VolumeControl.ActiveOutput.USB_SPEAKER: + if (mute || volume <= 0.0) + icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel"; + else if (this.accounts_service != null && this.accounts_service.silentMode) + icon = "audio-volume-muted-panel"; + else + icon = get_volume_root_icon_by_volume (volume, active_output); + break; + case VolumeControl.ActiveOutput.USB_HEADPHONES: + if (mute || volume <= 0.0) + icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel"; + else if (this.accounts_service != null && this.accounts_service.silentMode) + icon = "audio-volume-muted-panel"; + else + icon = get_volume_root_icon_by_volume (volume, active_output); + break; + case VolumeControl.ActiveOutput.HDMI_SPEAKER: + if (mute || volume <= 0.0) + icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel"; + else if (this.accounts_service != null && this.accounts_service.silentMode) + icon = "audio-volume-muted-panel"; + else + icon = get_volume_root_icon_by_volume (volume, active_output); + break; + case VolumeControl.ActiveOutput.HDMI_HEADPHONES: + if (mute || volume <= 0.0) + icon = this.mute_blocks_sound ? "audio-volume-muted-blocking-panel" : "audio-volume-muted-panel"; + else if (this.accounts_service != null && this.accounts_service.silentMode) + icon = "audio-volume-muted-panel"; + else + icon = get_volume_root_icon_by_volume (volume, active_output); + break; + } + return icon; + } - if (!notify_server_caps_checked) { - List<string> caps = Notify.get_server_caps (); - notify_server_supports_actions = caps.find_custom ("actions", strcmp) != null; - notify_server_supports_sync = caps.find_custom ("x-canonical-private-synchronous", strcmp) != null; - notify_server_caps_checked = true; + private string get_notification_label () { + string volume_label = ""; + switch (volume_control.active_output) + { + case VolumeControl.ActiveOutput.SPEAKERS: + volume_label = _("Speakers"); + break; + case VolumeControl.ActiveOutput.HEADPHONES: + volume_label = _("Headphones"); + break; + case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES: + volume_label = _("Bluetooth headphones"); + break; + case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER: + volume_label = _("Bluetooth speaker"); + break; + case VolumeControl.ActiveOutput.USB_SPEAKER: + volume_label = _("Usb speaker"); + break; + case VolumeControl.ActiveOutput.USB_HEADPHONES: + volume_label = _("Usb headphones"); + break; + case VolumeControl.ActiveOutput.HDMI_SPEAKER: + volume_label = _("HDMI speaker"); + break; + case VolumeControl.ActiveOutput.HDMI_HEADPHONES: + volume_label = _("HDMI headphones"); + break; } + return volume_label; + } + + private void update_notification () { + + List<string> caps = Notify.get_server_caps (); + notify_server_supports_actions = caps.find_custom ("actions", strcmp) != null; + notify_server_supports_sync = caps.find_custom ("x-canonical-private-synchronous", strcmp) != null; + notify_server_caps_checked = true; + var loud = volume_control.high_volume; var warn = loud && this.notify_server_supports_actions @@ -312,47 +626,36 @@ public class IndicatorSound.Service: Object { _pre_warn_volume = null; volume_control.volume = tmp; } + waiting_user_approve_warn = false; }); warn_notification.add_action ("cancel", _("Cancel"), (n, a) => { _pre_warn_volume = null; + waiting_user_approve_warn = false; }); + waiting_user_approve_warn = true; show_notification(warn_notification); } else { - close_notification(warn_notification); - - if (notify_server_supports_sync && !block_info_notifications) { - - /* Determine Label */ - unowned string volume_label = loud - ? _("High volume can damage your hearing.") - : ""; - - /* Choose an icon */ - unowned string icon; - if (loud) { - icon = "audio-volume-high"; - } else { - var volume = volume_control.volume.volume; - - if (volume <= 0.0) - icon = "audio-volume-muted"; - else if (volume <= 0.3) - icon = "audio-volume-low"; - else if (volume <= 0.7) - icon = "audio-volume-medium"; - else - icon = "audio-volume-high"; + if (!waiting_user_approve_warn) { + close_notification(warn_notification); + + if (notify_server_supports_sync && !block_info_notifications) { + + /* Determine Label */ + string volume_label = get_notification_label (); + + /* Choose an icon */ + string icon = get_volume_notification_icon (volume_control.volume.volume, loud, volume_control.active_output); + + /* Reset the notification */ + var n = this.info_notification; + n.update (_("Volume"), volume_label, icon); + n.clear_hints(); + n.set_hint ("x-canonical-non-shaped-icon", "true"); + n.set_hint ("x-canonical-private-synchronous", "true"); + n.set_hint ("x-canonical-value-bar-tint", loud ? "true" : "false"); + n.set_hint ("value", (int32)Math.round(get_volume_percent() * 100.0)); + show_notification(n); } - - /* Reset the notification */ - var n = this.info_notification; - n.update (_("Volume"), volume_label, icon); - n.clear_hints(); - n.set_hint ("x-canonical-non-shaped-icon", "true"); - n.set_hint ("x-canonical-private-synchronous", "true"); - n.set_hint ("x-canonical-value-bar-tint", loud ? "true" : "false"); - n.set_hint ("value", (int32)Math.round(get_volume_percent() * 100.0)); - show_notification(n); } } } diff --git a/src/sound-menu.vala b/src/sound-menu.vala index 7a6044b..3d682e4 100644 --- a/src/sound-menu.vala +++ b/src/sound-menu.vala @@ -71,6 +71,7 @@ public class SoundMenu: Object this.notify_handlers = new HashTable<MediaPlayer, ulong> (direct_hash, direct_equal); this.greeter_players = (flags & DisplayFlags.GREETER_PLAYERS) != 0; + } ~SoundMenu () { @@ -193,6 +194,43 @@ public class SoundMenu: Object this.notify_handlers.remove (player); } + public void update_volume_slider (VolumeControl.ActiveOutput active_output) { + int index = find_action (this.volume_section, "indicator.volume"); + if (index != -1) { + string label = "Volume"; + switch (active_output) { + case VolumeControl.ActiveOutput.SPEAKERS: + label = _("Volume"); + break; + case VolumeControl.ActiveOutput.HEADPHONES: + label = _("Volume (Headphones)"); + break; + case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER: + label = _("Volume (Bluetooth)"); + break; + case VolumeControl.ActiveOutput.USB_SPEAKER: + label = _("Volume (Usb)"); + break; + case VolumeControl.ActiveOutput.HDMI_SPEAKER: + label = _("Volume (HDMI)"); + break; + case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES: + label = _("Volume (Bluetooth headphones)"); + break; + case VolumeControl.ActiveOutput.USB_HEADPHONES: + label = _("Volume (Usb headphones)"); + break; + case VolumeControl.ActiveOutput.HDMI_HEADPHONES: + label = _("Volume (HDMI headphones)"); + break; + } + this.volume_section.remove (index); + this.volume_section.insert_item (index, this.create_slider_menu_item (_(label), "indicator.volume(0)", 0.0, 1.0, 0.01, + "audio-volume-low-zero-panel", + "audio-volume-high-panel")); + } + } + public Menu root; public Menu menu; Menu volume_section; diff --git a/src/volume-control-pulse.vala b/src/volume-control-pulse.vala index 87af258..8122f26 100644 --- a/src/volume-control-pulse.vala +++ b/src/volume-control-pulse.vala @@ -87,6 +87,7 @@ public class VolumeControlPulse : VolumeControl private bool _send_next_local_volume = false; private double _account_service_volume = 0.0; private bool _active_port_headphone = false; + private VolumeControl.ActiveOutput _active_output = VolumeControl.ActiveOutput.SPEAKERS; /** true when connected to the pulse server */ public override bool ready { get; private set; } @@ -135,6 +136,49 @@ public class VolumeControlPulse : VolumeControl stop_high_volume_approved_timer(); } + private VolumeControl.ActiveOutput calculate_active_output (SinkInfo? sink) { + + VolumeControl.ActiveOutput ret_output = VolumeControl.ActiveOutput.SPEAKERS; + /* Check if the current active port is headset/headphone */ + /* There is not easy way to check if the port is a headset/headphone besides + * checking for the port name. On touch (with the pulseaudio droid element) + * the headset/headphone port is called 'output-headset' and 'output-headphone'. + * On the desktop this is usually called 'analog-output-headphones' */ + // look if it's a headset/headphones + if (sink.name == "indicator_sound_test_headphones" || + (sink.active_port != null && + (sink.active_port.name.contains("headset") || + sink.active_port.name.contains("headphone")))) { + _active_port_headphone = true; + // check if it's a bluetooth device + var device_bus = sink.proplist.gets ("device.bus"); + if (device_bus != null && device_bus == "bluetooth") { + ret_output = VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES; + } else if (device_bus != null && device_bus == "usb") { + ret_output = VolumeControl.ActiveOutput.USB_HEADPHONES; + } else if (device_bus != null && device_bus == "hdmi") { + ret_output = VolumeControl.ActiveOutput.HDMI_HEADPHONES; + } else { + ret_output = VolumeControl.ActiveOutput.HEADPHONES; + } + } else { + // speaker + _active_port_headphone = false; + var device_bus = sink.proplist.gets ("device.bus"); + if (device_bus != null && device_bus == "bluetooth") { + ret_output = VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER; + } else if (device_bus != null && device_bus == "usb") { + ret_output = VolumeControl.ActiveOutput.USB_SPEAKER; + } else if (device_bus != null && device_bus == "hdmi") { + ret_output = VolumeControl.ActiveOutput.HDMI_SPEAKER; + } else { + ret_output = VolumeControl.ActiveOutput.SPEAKERS; + } + } + + return ret_output; + } + /* PulseAudio logic*/ private void context_events_cb (Context c, Context.SubscriptionEventType t, uint32 index) { @@ -201,18 +245,20 @@ public class VolumeControlPulse : VolumeControl this.notify_property ("is-playing"); } - /* Check if the current active port is headset/headphone */ - /* There is not easy way to check if the port is a headset/headphone besides - * checking for the port name. On touch (with the pulseaudio droid element) - * the headset/headphone port is called 'output-headset' and 'output-headphone'. - * On the desktop this is usually called 'analog-output-headphones' */ - if (i.active_port != null && - (i.active_port.name == "output-wired_headset" || - i.active_port.name == "output-wired_headphone" || - i.active_port.name == "analog-output-headphones")) { - _active_port_headphone = true; - } else { - _active_port_headphone = false; + // store the current status of the active output + VolumeControl.ActiveOutput active_output_before = active_output; + + // calculate the output + _active_output = calculate_active_output (i); + + // check if the output has changed, if so... emit a signal + VolumeControl.ActiveOutput active_output_now = active_output; + if (active_output_now != active_output_before) { + this.active_output_changed (active_output_now); + if (active_output_now == VolumeControl.ActiveOutput.SPEAKERS) { + _high_volume_approved = false; + } + update_high_volume(); } if (_pulse_use_stream_restore == false && @@ -478,7 +524,8 @@ public class VolumeControlPulse : VolumeControl this.context = new PulseAudio.Context (loop.get_api(), null, props); this.context.set_state_callback (context_state_callback); - if (context.connect(null, Context.Flags.NOFAIL, null) < 0) + var server_string = Environment.get_variable("PULSE_SERVER"); + if (context.connect(server_string, Context.Flags.NOFAIL, null) < 0) warning( "pa_context_connect() failed: %s\n", PulseAudio.strerror(context.errno())); } @@ -535,6 +582,14 @@ public class VolumeControlPulse : VolumeControl } } + public override VolumeControl.ActiveOutput active_output + { + get + { + return _active_output; + } + } + /* Volume operations */ private static PulseAudio.Volume double_to_volume (double vol) { @@ -710,7 +765,7 @@ public class VolumeControlPulse : VolumeControl private bool calculate_high_volume_from_volume(double volume) { return _active_port_headphone && _warning_volume_enabled - && volume >= _warning_volume_norms + && volume > _warning_volume_norms && (stream == "multimedia"); } diff --git a/src/volume-control.vala b/src/volume-control.vala index 6efac35..8e615ea 100644 --- a/src/volume-control.vala +++ b/src/volume-control.vala @@ -28,6 +28,17 @@ public abstract class VolumeControl : Object VOLUME_STREAM_CHANGE } + public enum ActiveOutput { + SPEAKERS, + HEADPHONES, + BLUETOOTH_HEADPHONES, + BLUETOOTH_SPEAKER, + USB_SPEAKER, + USB_HEADPHONES, + HDMI_SPEAKER, + HDMI_HEADPHONES + } + public class Volume : Object { public double volume; public VolumeReasons reason; @@ -39,6 +50,7 @@ public abstract class VolumeControl : Object public virtual bool high_volume { get { return false; } protected set { } } public virtual bool mute { get { return false; } } public virtual bool is_playing { get { return false; } } + public virtual VolumeControl.ActiveOutput active_output { get { return VolumeControl.ActiveOutput.SPEAKERS; } } private Volume _volume; public virtual Volume volume { get { return _volume; } set { } } public virtual double mic_volume { get { return 0.0; } set { } } @@ -56,4 +68,6 @@ public abstract class VolumeControl : Object v.reason = reason; this.volume = v; } + + public signal void active_output_changed (VolumeControl.ActiveOutput active_output); } |