blob: 37565b67ce7b72240ed2394baf2eb051fd54e68d [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/dbus/menu/menu.h"
#include <limits>
#include <memory>
#include <set>
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/i18n/rtl.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/utf_string_conversions.h"
#include "components/dbus/properties/dbus_properties.h"
#include "components/dbus/properties/success_barrier_callback.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/models/menu_model.h"
#include "ui/menus/simple_menu_model.h"
namespace {
// Interfaces.
const char kInterfaceDbusMenu[] = "com.canonical.dbusmenu";
// Methods.
const char kMethodAboutToShow[] = "AboutToShow";
const char kMethodAboutToShowGroup[] = "AboutToShowGroup";
const char kMethodEvent[] = "Event";
const char kMethodEventGroup[] = "EventGroup";
const char kMethodGetGroupProperties[] = "GetGroupProperties";
const char kMethodGetLayout[] = "GetLayout";
const char kMethodGetProperty[] = "GetProperty";
// Properties.
const char kPropertyIconThemePath[] = "IconThemePath";
const char kPropertyMenuStatus[] = "Status";
const char kPropertyTextDirection[] = "TextDirection";
const char kPropertyVersion[] = "Version";
// Property values.
const char kPropertyValueStatusNormal[] = "normal";
uint32_t kPropertyValueVersion = 3;
// Signals.
const char kSignalItemsPropertiesUpdated[] = "ItemsPropertiesUpdated";
const char kSignalLayoutUpdated[] = "LayoutUpdated";
// Creates a variant with the default value for |property_name|, or an empty
// variant if |property_name| is invalid.
DbusVariant CreateDefaultPropertyValue(const std::string& property_name) {
if (property_name == "type")
return MakeDbusVariant(DbusString("standard"));
if (property_name == "label")
return MakeDbusVariant(DbusString(""));
if (property_name == "enabled")
return MakeDbusVariant(DbusBoolean(true));
if (property_name == "visible")
return MakeDbusVariant(DbusBoolean(true));
if (property_name == "icon-name")
return MakeDbusVariant(DbusString(""));
if (property_name == "icon-data")
return MakeDbusVariant(DbusByteArray());
if (property_name == "shortcut")
return MakeDbusVariant(DbusArray<DbusArray<DbusString>>());
if (property_name == "toggle-type")
return MakeDbusVariant(DbusString(""));
if (property_name == "toggle-state")
return MakeDbusVariant(DbusInt32(-1));
if (property_name == "children-display")
return MakeDbusVariant(DbusString(""));
return DbusVariant();
}
DbusString DbusTextDirection() {
return DbusString(base::i18n::IsRTL() ? "rtl " : "ltr");
}
void WriteRemovedProperties(dbus::MessageWriter* writer,
const MenuPropertyChanges& removed_props) {
dbus::MessageWriter removed_props_writer(nullptr);
writer->OpenArray("(ias)", &removed_props_writer);
for (const auto& pair : removed_props) {
dbus::MessageWriter struct_writer(nullptr);
removed_props_writer.OpenStruct(&struct_writer);
struct_writer.AppendInt32(pair.first);
struct_writer.AppendArrayOfStrings(pair.second);
removed_props_writer.CloseContainer(&struct_writer);
}
writer->CloseContainer(&removed_props_writer);
}
} // namespace
DbusMenu::MenuItem::MenuItem(int32_t id,
std::map<std::string, DbusVariant>&& properties,
std::vector<int32_t>&& children,
ui::MenuModel* menu,
ui::MenuModel* containing_menu,
size_t containing_menu_index)
: id(id),
properties(std::move(properties)),
children(std::move(children)),
menu(menu),
containing_menu(containing_menu),
containing_menu_index(containing_menu_index) {}
DbusMenu::MenuItem::~MenuItem() = default;
DbusMenu::ScopedMethodResponse::ScopedMethodResponse(
dbus::MethodCall* method_call,
dbus::ExportedObject::ResponseSender response_sender)
: method_call_(method_call),
response_sender_(std::move(response_sender)),
reader_(method_call_) {}
DbusMenu::ScopedMethodResponse::~ScopedMethodResponse() {
// Reset method_call_, it may no longer be valid after Run().
method_call_ = nullptr;
std::move(response_sender_).Run(std::move(response_));
}
dbus::MessageWriter& DbusMenu::ScopedMethodResponse::Writer() {
EnsureResponse();
if (!writer_)
writer_ = std::make_unique<dbus::MessageWriter>(response_.get());
return *writer_;
}
void DbusMenu::ScopedMethodResponse::EnsureResponse() {
if (!response_)
response_ = dbus::Response::FromMethodCall(method_call_);
}
DbusMenu::DbusMenu(dbus::ExportedObject* exported_object,
InitializedCallback callback)
: menu_(exported_object) {
SetModel(nullptr, false);
static constexpr struct {
const char* name;
void (DbusMenu::*callback)(ScopedMethodResponse*);
} methods[7] = {
{kMethodAboutToShow, &DbusMenu::OnAboutToShow},
{kMethodAboutToShowGroup, &DbusMenu::OnAboutToShowGroup},
{kMethodEvent, &DbusMenu::OnEvent},
{kMethodEventGroup, &DbusMenu::OnEventGroup},
{kMethodGetGroupProperties, &DbusMenu::OnGetGroupProperties},
{kMethodGetLayout, &DbusMenu::OnGetLayout},
{kMethodGetProperty, &DbusMenu::OnGetProperty},
};
// std::size(methods) calls for method export, 1 call for properties
// initialization.
barrier_ =
SuccessBarrierCallback(std::size(methods) + 1, std::move(callback));
for (const auto& method : methods) {
menu_->ExportMethod(
kInterfaceDbusMenu, method.name,
WrapMethodCallback(
base::BindRepeating(method.callback, weak_factory_.GetWeakPtr())),
base::BindRepeating(&DbusMenu::OnExported, weak_factory_.GetWeakPtr()));
}
properties_ = std::make_unique<DbusProperties>(menu_, barrier_);
properties_->RegisterInterface(kInterfaceDbusMenu);
auto set_property = [&](const std::string& property_name, auto&& value) {
properties_->SetProperty(kInterfaceDbusMenu, property_name,
std::forward<decltype(value)>(value), false);
};
set_property(kPropertyIconThemePath, DbusArray<DbusString>());
set_property(kPropertyMenuStatus, DbusString(kPropertyValueStatusNormal));
set_property(kPropertyTextDirection, DbusTextDirection());
set_property(kPropertyVersion, DbusUint32(kPropertyValueVersion));
}
DbusMenu::~DbusMenu() = default;
void DbusMenu::SetModel(ui::MenuModel* model, bool send_signal) {
items_.clear();
std::map<std::string, DbusVariant> properties;
std::vector<int32_t> children;
if (model) {
properties["children-display"] = MakeDbusVariant(DbusString("submenu"));
children = ConvertMenu(model);
}
items_[0] = std::make_unique<MenuItem>(
0, std::move(properties), std::move(children), nullptr, nullptr, -1);
if (send_signal)
SendLayoutChangedSignal(0);
}
void DbusMenu::MenuLayoutUpdated(ui::MenuModel* model) {
MenuItem* item = FindMenuItemForModel(model, items_[0].get());
DCHECK(item);
DeleteItemChildren(item);
item->children = ConvertMenu(model);
SendLayoutChangedSignal(item->id);
}
void DbusMenu::MenuItemsPropertiesUpdated(
const std::vector<MenuItemReference>& menu_items) {
if (menu_items.empty())
return;
MenuPropertyChanges updated_props;
MenuPropertyChanges removed_props;
for (const auto& menu_item : menu_items) {
ui::MenuModel* menu = menu_item.first;
size_t index = menu_item.second;
MenuItem* parent = FindMenuItemForModel(menu, items_[0].get());
MenuItem* item = nullptr;
for (int32_t id : parent->children) {
MenuItem* child = items_[id].get();
DCHECK_EQ(child->containing_menu, menu);
if (child->containing_menu_index == index) {
item = child;
break;
}
}
DCHECK(item);
auto old_properties = std::move(item->properties);
item->properties = ComputeMenuPropertiesForMenuItem(menu, index);
MenuPropertyList item_updated_props;
MenuPropertyList item_removed_props;
ComputeMenuPropertyChanges(old_properties, item->properties,
&item_updated_props, &item_removed_props);
if (!item_updated_props.empty())
updated_props[item->id] = std::move(item_updated_props);
if (!item_removed_props.empty())
removed_props[item->id] = std::move(item_removed_props);
}
dbus::Signal signal(kInterfaceDbusMenu, kSignalItemsPropertiesUpdated);
dbus::MessageWriter writer(&signal);
WriteUpdatedProperties(&writer, updated_props);
WriteRemovedProperties(&writer, removed_props);
menu_->SendSignal(&signal);
}
// static
dbus::ExportedObject::MethodCallCallback DbusMenu::WrapMethodCallback(
base::RepeatingCallback<void(ScopedMethodResponse*)> callback) {
return base::BindRepeating(
[](base::RepeatingCallback<void(ScopedMethodResponse*)> bound_callback,
dbus::MethodCall* method_call,
dbus::ExportedObject::ResponseSender response_sender) {
ScopedMethodResponse response(method_call, std::move(response_sender));
bound_callback.Run(&response);
},
callback);
}
void DbusMenu::OnExported(const std::string& interface_name,
const std::string& method_name,
bool success) {
barrier_.Run(success);
}
void DbusMenu::OnAboutToShow(ScopedMethodResponse* response) {
int32_t id;
if (!response->reader().PopInt32(&id))
return;
if (!AboutToShowImpl(id))
return;
response->Writer().AppendBool(false);
}
void DbusMenu::OnAboutToShowGroup(ScopedMethodResponse* response) {
dbus::MessageReader array_reader(nullptr);
if (!response->reader().PopArray(&array_reader))
return;
std::vector<int32_t> ids;
while (array_reader.HasMoreData()) {
int32_t id;
if (!array_reader.PopInt32(&id))
return;
ids.push_back(id);
}
std::vector<int32_t> id_errors;
for (int32_t id : ids) {
if (!AboutToShowImpl(id))
id_errors.push_back(id);
}
// IDs of updates needed (none).
response->Writer().AppendArrayOfInt32s({});
// Invalid IDs.
response->Writer().AppendArrayOfInt32s(id_errors);
}
void DbusMenu::OnEvent(ScopedMethodResponse* response) {
if (!EventImpl(&response->reader(), nullptr))
return;
response->EnsureResponse();
}
void DbusMenu::OnEventGroup(ScopedMethodResponse* response) {
dbus::MessageReader array_reader(nullptr);
if (!response->reader().PopArray(&array_reader))
return;
std::vector<int32_t> id_errors;
while (array_reader.HasMoreData()) {
dbus::MessageReader struct_reader(nullptr);
array_reader.PopStruct(&struct_reader);
int32_t id_error = -1;
if (!EventImpl(&struct_reader, &id_error)) {
if (id_error < 0)
return;
id_errors.push_back(id_error);
}
}
response->Writer().AppendArrayOfInt32s(id_errors);
}
void DbusMenu::OnGetGroupProperties(ScopedMethodResponse* response) {
dbus::MessageReader id_reader(nullptr);
if (!response->reader().PopArray(&id_reader))
return;
std::vector<int32_t> ids;
while (id_reader.HasMoreData()) {
int32_t id;
if (!id_reader.PopInt32(&id))
return;
ids.push_back(id);
}
std::set<std::string> property_filter;
dbus::MessageReader property_reader(nullptr);
if (!response->reader().PopArray(&property_reader))
return;
while (property_reader.HasMoreData()) {
std::string property;
if (!property_reader.PopString(&property))
return;
property_filter.insert(property);
}
dbus::MessageWriter& writer = response->Writer();
dbus::MessageWriter item_writer(nullptr);
writer.OpenArray("(ia{sv})", &item_writer);
auto write_item = [&](int32_t id, const MenuItem& item) {
dbus::MessageWriter struct_writer(nullptr);
item_writer.OpenStruct(&struct_writer);
struct_writer.AppendInt32(id);
dbus::MessageWriter property_writer(nullptr);
struct_writer.OpenArray("{sv}", &property_writer);
for (const auto& property_pair : item.properties) {
if (!property_filter.empty() &&
!base::Contains(property_filter, property_pair.first)) {
continue;
}
dbus::MessageWriter dict_entry_writer(nullptr);
property_writer.OpenDictEntry(&dict_entry_writer);
dict_entry_writer.AppendString(property_pair.first);
property_pair.second.Write(&dict_entry_writer);
property_writer.CloseContainer(&dict_entry_writer);
}
struct_writer.CloseContainer(&property_writer);
item_writer.CloseContainer(&struct_writer);
};
if (ids.empty()) {
for (const auto& item_pair : items_)
write_item(item_pair.first, *item_pair.second);
} else {
for (int32_t id : ids) {
auto it = items_.find(id);
if (it != items_.end())
write_item(id, *it->second);
}
}
writer.CloseContainer(&item_writer);
}
void DbusMenu::OnGetLayout(ScopedMethodResponse* response) {
dbus::MessageReader& reader = response->reader();
int32_t id;
int32_t depth;
MenuPropertyList property_filter;
if (!reader.PopInt32(&id) || !reader.PopInt32(&depth) || depth < -1 ||
!reader.PopArrayOfStrings(&property_filter)) {
return;
}
auto it = items_.find(id);
if (it == items_.end())
return;
dbus::MessageWriter& writer = response->Writer();
writer.AppendUint32(revision_);
WriteMenuItem(it->second.get(), &writer, depth, property_filter);
}
void DbusMenu::OnGetProperty(ScopedMethodResponse* response) {
dbus::MessageReader& reader = response->reader();
int32_t id;
std::string name;
if (!reader.PopInt32(&id) || !reader.PopString(&name))
return;
auto item_it = items_.find(id);
if (item_it == items_.end())
return;
MenuItem* item = item_it->second.get();
auto property_it = item->properties.find(name);
if (property_it == item->properties.end()) {
DbusVariant default_value = CreateDefaultPropertyValue(name);
if (default_value)
default_value.Write(&response->Writer());
} else {
property_it->second.Write(&response->Writer());
}
}
bool DbusMenu::AboutToShowImpl(int32_t id) {
auto item = items_.find(id);
if (item == items_.end())
return false;
if (id != 0) {
ui::MenuModel* menu = item->second->menu;
if (!menu) {
return false;
}
menu->MenuWillShow();
}
return true;
}
bool DbusMenu::EventImpl(dbus::MessageReader* reader, int32_t* id_error) {
int32_t id;
if (!reader->PopInt32(&id))
return false;
auto item_it = items_.find(id);
if (item_it == items_.end()) {
if (id_error)
*id_error = id;
return false;
}
std::string type;
if (!reader->PopString(&type))
return false;
if (type == "clicked") {
MenuItem* item = item_it->second.get();
if (!item->containing_menu)
return false;
item->containing_menu->ActivatedAt(item->containing_menu_index);
} else {
DCHECK(type == "hovered" || type == "opened" || type == "closed")
<< "Unexpected type: " << type;
// Nothing to do.
}
return true;
}
std::vector<int32_t> DbusMenu::ConvertMenu(ui::MenuModel* menu) {
std::vector<int32_t> items;
if (!menu)
return items;
items.reserve(menu->GetItemCount());
for (size_t i = 0; i < menu->GetItemCount(); ++i) {
ui::MenuModel* submenu = menu->GetSubmenuModelAt(i);
std::vector<int32_t> children = ConvertMenu(submenu);
int32_t id = NextItemId();
items_[id] = std::make_unique<MenuItem>(
id, ComputeMenuPropertiesForMenuItem(menu, i), std::move(children),
submenu, menu, i);
items.push_back(id);
}
return items;
}
int32_t DbusMenu::NextItemId() {
return last_item_id_ = last_item_id_ == std::numeric_limits<int32_t>::max()
? 1
: last_item_id_ + 1;
}
void DbusMenu::WriteMenuItem(const MenuItem* item,
dbus::MessageWriter* writer,
int32_t depth,
const MenuPropertyList& property_filter) const {
dbus::MessageWriter struct_writer(nullptr);
writer->OpenStruct(&struct_writer);
struct_writer.AppendInt32(item->id);
dbus::MessageWriter properties_writer(nullptr);
struct_writer.OpenArray("{sv}", &properties_writer);
for (const auto& property : item->properties) {
if (property_filter.empty() ||
base::Contains(property_filter, property.first)) {
dbus::MessageWriter dict_entry_writer(nullptr);
properties_writer.OpenDictEntry(&dict_entry_writer);
dict_entry_writer.AppendString(property.first);
property.second.Write(&dict_entry_writer);
properties_writer.CloseContainer(&dict_entry_writer);
}
}
struct_writer.CloseContainer(&properties_writer);
dbus::MessageWriter children_writer(nullptr);
struct_writer.OpenArray("v", &children_writer);
if (depth != 0) {
for (int32_t child : item->children) {
dbus::MessageWriter variant_writer(nullptr);
children_writer.OpenVariant("(ia{sv}av)", &variant_writer);
WriteMenuItem(items_.find(child)->second.get(), &variant_writer,
depth == -1 ? -1 : depth - 1, property_filter);
children_writer.CloseContainer(&variant_writer);
}
}
struct_writer.CloseContainer(&children_writer);
writer->CloseContainer(&struct_writer);
}
void DbusMenu::WriteUpdatedProperties(
dbus::MessageWriter* writer,
const MenuPropertyChanges& updated_props) const {
dbus::MessageWriter updated_props_writer(nullptr);
writer->OpenArray("(ia{sv})", &updated_props_writer);
for (const auto& pair : updated_props) {
int32_t id = pair.first;
MenuItem* item = items_.find(id)->second.get();
dbus::MessageWriter struct_writer(nullptr);
updated_props_writer.OpenStruct(&struct_writer);
struct_writer.AppendInt32(id);
dbus::MessageWriter array_writer(nullptr);
struct_writer.OpenArray("{sv}", &array_writer);
for (const std::string& key : pair.second) {
dbus::MessageWriter dict_entry_writer(nullptr);
array_writer.OpenDictEntry(&dict_entry_writer);
dict_entry_writer.AppendString(key);
item->properties[key].Write(&dict_entry_writer);
array_writer.CloseContainer(&dict_entry_writer);
}
struct_writer.CloseContainer(&array_writer);
updated_props_writer.CloseContainer(&struct_writer);
}
writer->CloseContainer(&updated_props_writer);
}
DbusMenu::MenuItem* DbusMenu::FindMenuItemForModel(const ui::MenuModel* model,
MenuItem* item) const {
if (item->menu == model)
return item;
for (int32_t id : item->children) {
MenuItem* child = items_.find(id)->second.get();
MenuItem* found = FindMenuItemForModel(model, child);
if (found)
return found;
}
return nullptr;
}
void DbusMenu::DeleteItem(MenuItem* item) {
DeleteItemChildren(item);
items_.erase(item->id);
}
void DbusMenu::DeleteItemChildren(MenuItem* item) {
for (int32_t id : item->children)
DeleteItem(items_.find(id)->second.get());
}
void DbusMenu::SendLayoutChangedSignal(int32_t id) {
dbus::Signal signal(kInterfaceDbusMenu, kSignalLayoutUpdated);
dbus::MessageWriter writer(&signal);
writer.AppendUint32(++revision_); // Revision of the new layout.
writer.AppendInt32(id); // Parent item whose layout changed.
menu_->SendSignal(&signal);
}