blob: 5d409095512f0d43148567ab54b1d247fafaf9a7 [file]
// Copyright 2019 The Chromium Authors. All rights reserved.
// 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 "base/bind.h"
#include "base/callback.h"
#include "base/i18n/rtl.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/scoped_refptr.h"
#include "base/stl_util.h"
#include "base/strings/utf_string_conversions.h"
#include "components/dbus/menu/properties_interface.h"
#include "components/dbus/menu/success_barrier_callback.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/accelerators/menu_label_accelerator_util_linux.h"
#include "ui/base/models/menu_model.h"
#include "ui/base/models/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 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");
}
} // 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,
int 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_(response_sender),
reader_(method_call_) {}
DbusMenu::ScopedMethodResponse::~ScopedMethodResponse() {
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},
};
// base::size(methods) calls for method export, 1 call for properties
// initialization.
barrier_ =
SuccessBarrierCallback(base::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<DbusPropertiesInterface>(menu_, barrier_);
properties_->RegisterInterface(kInterfaceDbusMenu);
auto set_property = [&](const std::string& property_name, auto&& value) {
properties_->SetProperty(kInterfaceDbusMenu, property_name,
std::move(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();
if (model) {
std::map<std::string, DbusVariant> properties;
properties["children-display"] = MakeDbusVariant(DbusString("submenu"));
items_[0] = std::make_unique<MenuItem>(
0, std::move(properties), ConvertMenu(model), nullptr, nullptr, -1);
} else {
items_[0] = std::make_unique<MenuItem>(
0, std::map<std::string, DbusVariant>(), std::vector<int32_t>(),
nullptr, nullptr, -1);
}
if (send_signal) {
dbus::Signal signal(kInterfaceDbusMenu, kSignalLayoutUpdated);
dbus::MessageWriter writer(&signal);
writer.AppendUint32(++revision_); // Revision of the new layout.
writer.AppendInt32(0); // Parent item whose layout changed.
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, &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(nullptr, 0);
// Invalid IDs.
response->Writer().AppendArrayOfInt32s(id_errors.data(), id_errors.size());
}
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.data(), id_errors.size());
}
void DbusMenu::OnGetGroupProperties(ScopedMethodResponse* response) {
dbus::MessageReader id_reader(nullptr);
if (!response->reader().PopArray(&id_reader))
return;
std::set<int32_t> ids;
while (id_reader.HasMoreData()) {
int32_t id;
if (!id_reader.PopInt32(&id))
return;
ids.insert(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);
for (const auto& item_pair : items_) {
if (!ids.empty() && !base::Contains(ids, item_pair.first))
continue;
dbus::MessageWriter struct_writer(nullptr);
item_writer.OpenStruct(&struct_writer);
struct_writer.AppendInt32(item_pair.first);
dbus::MessageWriter property_writer(nullptr);
struct_writer.OpenArray("{sv}", &property_writer);
for (const auto& property_pair : item_pair.second->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);
}
writer.CloseContainer(&item_writer);
}
void DbusMenu::OnGetLayout(ScopedMethodResponse* response) {
dbus::MessageReader& reader = response->reader();
int32_t id;
int32_t depth;
std::vector<std::string> 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;
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_EQ("hovered", 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 (int i = 0; i < menu->GetItemCount(); ++i) {
// Properties should only be set if they differ from the default values.
std::map<std::string, DbusVariant> properties;
// The dbusmenu interface has no concept of a "sublabel", "minor text", or
// "minor icon" like MenuModel has. Ignore these rather than trying to
// merge them with the regular label and icon.
base::string16 label = menu->GetLabelAt(i);
if (!label.empty()) {
properties["label"] = MakeDbusVariant(DbusString(
ui::ConvertAcceleratorsFromWindowsStyle(base::UTF16ToUTF8(label))));
}
if (!menu->IsEnabledAt(i))
properties["enabled"] = MakeDbusVariant(DbusBoolean(false));
if (!menu->IsVisibleAt(i))
properties["visible"] = MakeDbusVariant(DbusBoolean(false));
gfx::Image icon;
if (menu->GetIconAt(i, &icon)) {
properties["icon-data"] =
MakeDbusVariant(DbusByteArray(icon.As1xPNGBytes()));
}
ui::Accelerator accelerator;
if (menu->GetAcceleratorAt(i, &accelerator)) {
std::vector<DbusString> parts;
if (accelerator.IsCtrlDown())
parts.push_back(DbusString("Control"));
if (accelerator.IsAltDown())
parts.push_back(DbusString("Alt"));
if (accelerator.IsShiftDown())
parts.push_back(DbusString("Shift"));
if (accelerator.IsCmdDown())
parts.push_back(DbusString("Super"));
parts.push_back(
DbusString(base::UTF16ToUTF8(accelerator.KeyCodeToName())));
properties["shortcut"] = MakeDbusVariant(
MakeDbusArray(DbusArray<DbusString>(std::move(parts))));
}
switch (menu->GetTypeAt(i)) {
case ui::MenuModel::TYPE_COMMAND:
case ui::MenuModel::TYPE_HIGHLIGHTED:
// Nothing special to do.
break;
case ui::MenuModel::TYPE_CHECK:
case ui::MenuModel::TYPE_RADIO:
properties["toggle-type"] = MakeDbusVariant(DbusString(
menu->GetTypeAt(i) == ui::MenuModel::TYPE_CHECK ? "checkmark"
: "radio"));
properties["toggle-state"] =
MakeDbusVariant(DbusInt32(menu->IsItemCheckedAt(i) ? 1 : 0));
break;
case ui::MenuModel::TYPE_SEPARATOR:
// The dbusmenu interface doesn't have multiple types of separators like
// MenuModel. Just use a regular separator in all cases.
properties["type"] = MakeDbusVariant(DbusString("separator"));
break;
case ui::MenuModel::TYPE_BUTTON_ITEM:
// This type of menu represents a row of buttons, but the dbusmenu
// interface has no equivalent of this. Ignore these items for now
// since there's currently no uses of it that plumb into this codepath.
// If there are button menu items in the future, we'd have to fake them
// with multiple menu items.
NOTIMPLEMENTED();
continue;
case ui::MenuModel::TYPE_SUBMENU:
case ui::MenuModel::TYPE_ACTIONABLE_SUBMENU:
properties["children-display"] = MakeDbusVariant(DbusString("submenu"));
break;
}
ui::MenuModel* submenu = menu->GetSubmenuModelAt(i);
std::vector<int32_t> children = ConvertMenu(submenu);
int32_t id = NextItemId();
items_[id] = std::make_unique<MenuItem>(
id, std::move(properties), 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 std::vector<std::string>& property_filter) {
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_[child].get(), &variant_writer,
depth == -1 ? -1 : depth - 1, property_filter);
children_writer.CloseContainer(&variant_writer);
}
}
struct_writer.CloseContainer(&children_writer);
writer->CloseContainer(&struct_writer);
}