aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt19
-rw-r--r--src/gmenuharness/CMakeLists.txt17
-rw-r--r--src/gmenuharness/MatchResult.cpp187
-rw-r--r--src/gmenuharness/MatchUtils.cpp77
-rw-r--r--src/gmenuharness/MenuItemMatcher.cpp1008
-rw-r--r--src/gmenuharness/MenuMatcher.cpp208
-rw-r--r--src/media-player-mpris.vala22
-rw-r--r--src/media-player.vala4
-rw-r--r--src/mpris2-interfaces.vala5
-rw-r--r--src/sound-menu.vala84
-rw-r--r--src/volume-control-pulse.vala70
11 files changed, 1648 insertions, 53 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index a0f458d..73a270c 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
@@ -165,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(
@@ -207,3 +207,4 @@ install(
RUNTIME DESTINATION ${CMAKE_INSTALL_LIBEXECDIR}/indicator-sound/
)
+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/media-player-mpris.vala b/src/media-player-mpris.vala
index 741d887..780d724 100644
--- a/src/media-player-mpris.vala
+++ b/src/media-player-mpris.vala
@@ -79,6 +79,24 @@ public class MediaPlayerMpris: MediaPlayer {
}
}
+ public override bool can_do_play {
+ get {
+ return this.proxy.CanPlay;
+ }
+ }
+
+ public override bool can_do_prev {
+ get {
+ return this.proxy.CanGoPrevious;
+ }
+ }
+
+ public override bool can_do_next {
+ get {
+ return this.proxy.CanGoNext;
+ }
+ }
+
/**
* Attach this object to a process of the associated media player. The player must own @dbus_name and
* implement the org.mpris.MediaPlayer2.Player interface.
@@ -272,6 +290,10 @@ public class MediaPlayerMpris: MediaPlayer {
if (changed_properties.lookup ("PlaybackStatus", "s", null)) {
this.state = this.proxy.PlaybackStatus != null ? this.proxy.PlaybackStatus : "Unknown";
}
+ if (changed_properties.lookup ("CanGoNext", "b", null) || changed_properties.lookup ("CanGoPrevious", "b", null) ||
+ changed_properties.lookup ("CanPlay", "b", null) || changed_properties.lookup ("CanPause", "b", null) ) {
+ this.playbackstatus_changed ();
+ }
var metadata = changed_properties.lookup_value ("Metadata", new VariantType ("a{sv}"));
if (metadata != null)
diff --git a/src/media-player.vala b/src/media-player.vala
index 4d4aef3..04d1426 100644
--- a/src/media-player.vala
+++ b/src/media-player.vala
@@ -26,6 +26,9 @@ public abstract class MediaPlayer : Object {
public virtual bool is_running { get { not_implemented(); return false; } }
public virtual bool can_raise { get { not_implemented(); return false; } }
+ public virtual bool can_do_next { get { not_implemented(); return false; } }
+ public virtual bool can_do_prev { get { not_implemented(); return false; } }
+ public virtual bool can_do_play { get { not_implemented(); return false; } }
public class Track : Object {
public string artist { get; construct; }
@@ -44,6 +47,7 @@ public abstract class MediaPlayer : Object {
}
public signal void playlists_changed ();
+ public signal void playbackstatus_changed ();
public abstract void activate ();
public abstract void play_pause ();
diff --git a/src/mpris2-interfaces.vala b/src/mpris2-interfaces.vala
index a472d5c..0ed8719 100644
--- a/src/mpris2-interfaces.vala
+++ b/src/mpris2-interfaces.vala
@@ -37,7 +37,10 @@ public interface MprisPlayer : Object {
// properties
public abstract HashTable<string, Variant?> Metadata{owned get; set;}
public abstract int32 Position{owned get; set;}
- public abstract string? PlaybackStatus{owned get; set;}
+ public abstract string? PlaybackStatus{owned get; set;}
+ public abstract bool CanPlay{owned get; set;}
+ public abstract bool CanGoNext{owned get; set;}
+ public abstract bool CanGoPrevious{owned get; set;}
// methods
public abstract async void PlayPause() throws IOError;
public abstract async void Next() throws IOError;
diff --git a/src/sound-menu.vala b/src/sound-menu.vala
index b4e3e2a..b612264 100644
--- a/src/sound-menu.vala
+++ b/src/sound-menu.vala
@@ -158,18 +158,26 @@ public class SoundMenu: Object
this.update_playlists (player);
var handler_id = player.notify["is-running"].connect ( () => {
- if (player.is_running)
- if (this.find_player_section(player) == -1)
+ if (player.is_running) {
+ int index = this.find_player_section(player);
+ if (index == -1) {
this.insert_player_section (player);
- else
+ }
+ else {
+ update_player_section (player, index);
+ }
+ }
+ else {
if (this.hide_inactive)
this.remove_player_section (player);
+ }
this.update_playlists (player);
});
this.notify_handlers.insert (player, handler_id);
player.playlists_changed.connect (this.update_playlists);
+ player.playbackstatus_changed.connect (this.update_playbackstatus);
}
public void remove_player (MediaPlayer player) {
@@ -197,9 +205,24 @@ public class SoundMenu: Object
case VolumeControl.ActiveOutput.HEADPHONES:
label = "Volume (Headphones)";
break;
- case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES:
+ 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,
@@ -238,6 +261,23 @@ public class SoundMenu: Object
return -1;
}
+ MenuItem create_playback_menu_item (MediaPlayer player) {
+ var playback_item = new MenuItem (null, null);
+ playback_item.set_attribute ("x-canonical-type", "s", "com.canonical.unity.playback-item");
+ if (player.is_running) {
+ if (player.can_do_play) {
+ playback_item.set_attribute ("x-canonical-play-action", "s", "indicator.play." + player.id);
+ }
+ if (player.can_do_next) {
+ playback_item.set_attribute ("x-canonical-next-action", "s", "indicator.next." + player.id);
+ }
+ if (player.can_do_prev) {
+ playback_item.set_attribute ("x-canonical-previous-action", "s", "indicator.previous." + player.id);
+ }
+ }
+ return playback_item;
+ }
+
void insert_player_section (MediaPlayer player) {
if (this.hide_players)
return;
@@ -263,9 +303,21 @@ public class SoundMenu: Object
var playback_item = new MenuItem (null, null);
playback_item.set_attribute ("x-canonical-type", "s", "com.canonical.unity.playback-item");
- playback_item.set_attribute ("x-canonical-play-action", "s", "indicator.play." + player.id);
- playback_item.set_attribute ("x-canonical-next-action", "s", "indicator.next." + player.id);
- playback_item.set_attribute ("x-canonical-previous-action", "s", "indicator.previous." + player.id);
+ playback_item.set_attribute ("x-canonical-play-action", "s", "indicator.play." + player.id + ".disabled");
+ playback_item.set_attribute ("x-canonical-next-action", "s", "indicator.next." + player.id + ".disabled");
+ playback_item.set_attribute ("x-canonical-previous-action", "s", "indicator.previous." + player.id + ".disabled");
+
+ if (player.is_running) {
+ if (player.can_do_play) {
+ playback_item.set_attribute ("x-canonical-play-action", "s", "indicator.play." + player.id);
+ }
+ if (player.can_do_next) {
+ playback_item.set_attribute ("x-canonical-next-action", "s", "indicator.next." + player.id);
+ }
+ if (player.can_do_prev) {
+ playback_item.set_attribute ("x-canonical-previous-action", "s", "indicator.previous." + player.id);
+ }
+ }
section.append_item (playback_item);
/* Add new players to the end of the player sections, just before the settings */
@@ -285,6 +337,17 @@ public class SoundMenu: Object
this.menu.remove (index);
}
+ void update_player_section (MediaPlayer player, int index) {
+ var player_section = this.menu.get_item_link(index, Menu.LINK_SECTION) as Menu;
+ if (player_section.get_n_items () == 2) {
+ // we have 2 items, the second one is the playback item
+ // remove it first
+ player_section.remove (1);
+ MenuItem playback_item = create_playback_menu_item (player);
+ player_section.append_item (playback_item);
+ }
+ }
+
void update_playlists (MediaPlayer player) {
int index = find_player_section (player);
if (index < 0)
@@ -315,6 +378,13 @@ public class SoundMenu: Object
submenu.append_section (null, playlists_section);
player_section.append_submenu (_("Choose Playlist"), submenu);
}
+
+ void update_playbackstatus (MediaPlayer player) {
+ int index = find_player_section (player);
+ if (index != -1) {
+ update_player_section (player, index);
+ }
+ }
MenuItem create_slider_menu_item (string label, string action, double min, double max, double step, string min_icon_name, string max_icon_name) {
var min_icon = new ThemedIcon.with_default_fallbacks (min_icon_name);
diff --git a/src/volume-control-pulse.vala b/src/volume-control-pulse.vala
index a1f743b..8122f26 100644
--- a/src/volume-control-pulse.vala
+++ b/src/volume-control-pulse.vala
@@ -144,41 +144,38 @@ public class VolumeControlPulse : VolumeControl
* 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 (sink.active_port != null) {
- // look if it's a headset/headphones
- if (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;
- }
+ // 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 {
- // 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;
- }
- }
- } else {
- _active_port_headphone = false;
- ret_output = VolumeControl.ActiveOutput.SPEAKERS;
- }
-
+ 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;
}
@@ -527,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()));
}
@@ -767,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");
}