blob: 409865ee61e11e3be15d34d33f7f96d95db51cd5 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/views/controls/menu/menu_model_adapter.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/memory/raw_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/base/models/menu_model.h"
#include "ui/base/models/menu_model_delegate.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/controls/menu/submenu_view.h"
#include "ui/views/test/views_test_base.h"
namespace {
// Base command id for test menu and its submenu.
constexpr int kRootIdBase = 100;
constexpr int kSubmenuIdBase = 200;
constexpr int kActionableSubmenuIdBase = 300;
class MenuModelBase : public ui::MenuModel {
public:
explicit MenuModelBase(int command_id_base)
: command_id_base_(command_id_base) {}
MenuModelBase(const MenuModelBase&) = delete;
MenuModelBase& operator=(const MenuModelBase&) = delete;
~MenuModelBase() override = default;
// ui::MenuModel implementation:
size_t GetItemCount() const override { return items_.size(); }
ItemType GetTypeAt(size_t index) const override { return items_[index].type; }
ui::MenuSeparatorType GetSeparatorTypeAt(size_t index) const override {
return ui::NORMAL_SEPARATOR;
}
int GetCommandIdAt(size_t index) const override {
return static_cast<int>(index) + command_id_base_;
}
std::u16string GetLabelAt(size_t index) const override {
return items_[index].label;
}
bool IsItemDynamicAt(size_t index) const override { return false; }
bool GetAcceleratorAt(size_t index,
ui::Accelerator* accelerator) const override {
return false;
}
bool IsItemCheckedAt(size_t index) const override { return false; }
int GetGroupIdAt(size_t index) const override { return 0; }
ui::ImageModel GetIconAt(size_t index) const override {
return ui::ImageModel();
}
ui::ButtonMenuItemModel* GetButtonMenuItemAt(size_t index) const override {
return nullptr;
}
bool IsEnabledAt(size_t index) const override {
return items_[index].enabled;
}
bool IsVisibleAt(size_t index) const override {
return items_[index].visible;
}
bool IsAlertedAt(size_t index) const override {
return items_[index].alerted;
}
bool IsNewFeatureAt(size_t index) const override {
return items_[index].new_feature;
}
MenuModel* GetSubmenuModelAt(size_t index) const override {
return items_[index].submenu;
}
void ActivatedAt(size_t index) override { set_last_activation(index); }
void ActivatedAt(size_t index, int event_flags) override {
ActivatedAt(index);
}
void MenuWillShow() override {}
void MenuWillClose() override {}
// Item definition.
struct Item {
Item(ItemType item_type,
const std::string& item_label,
ui::MenuModel* item_submenu)
: type(item_type),
label(base::ASCIIToUTF16(item_label)),
submenu(item_submenu) {}
Item(ItemType item_type,
const std::string& item_label,
ui::MenuModel* item_submenu,
bool enabled,
bool visible)
: type(item_type),
label(base::ASCIIToUTF16(item_label)),
submenu(item_submenu),
enabled(enabled),
visible(visible) {}
ItemType type;
std::u16string label;
raw_ptr<ui::MenuModel> submenu;
bool enabled = true;
bool visible = true;
bool alerted = false;
bool new_feature = false;
};
const Item& GetItemDefinition(size_t index) { return items_[index]; }
// Access index argument to ActivatedAt().
std::optional<size_t> last_activation() const { return last_activation_; }
void set_last_activation(std::optional<size_t> last_activation) {
last_activation_ = last_activation;
}
protected:
std::vector<Item> items_;
private:
int command_id_base_;
std::optional<size_t> last_activation_;
};
class SubmenuModel final : public MenuModelBase {
public:
SubmenuModel() : MenuModelBase(kSubmenuIdBase) {
items_.emplace_back(TYPE_COMMAND, "submenu item 0", nullptr, false, true);
items_.emplace_back(TYPE_COMMAND, "submenu item 1", nullptr);
items_[1].alerted = true;
}
SubmenuModel(const SubmenuModel&) = delete;
SubmenuModel& operator=(const SubmenuModel&) = delete;
~SubmenuModel() override = default;
base::WeakPtr<ui::MenuModel> AsWeakPtr() override {
return weak_ptr_factory_.GetWeakPtr();
}
private:
base::WeakPtrFactory<SubmenuModel> weak_ptr_factory_{this};
};
class ActionableSubmenuModel final : public MenuModelBase {
public:
ActionableSubmenuModel() : MenuModelBase(kActionableSubmenuIdBase) {
items_.emplace_back(TYPE_COMMAND, "actionable submenu item 0", nullptr);
items_.emplace_back(TYPE_COMMAND, "actionable submenu item 1", nullptr);
items_[1].new_feature = true;
}
ActionableSubmenuModel(const ActionableSubmenuModel&) = delete;
ActionableSubmenuModel& operator=(const ActionableSubmenuModel&) = delete;
~ActionableSubmenuModel() override = default;
base::WeakPtr<ui::MenuModel> AsWeakPtr() override {
return weak_ptr_factory_.GetWeakPtr();
}
private:
base::WeakPtrFactory<ActionableSubmenuModel> weak_ptr_factory_{this};
};
class RootModel final : public MenuModelBase {
public:
RootModel() : MenuModelBase(kRootIdBase) {
submenu_model_ = std::make_unique<SubmenuModel>();
actionable_submenu_model_ = std::make_unique<ActionableSubmenuModel>();
items_.emplace_back(TYPE_COMMAND, "command 0", nullptr, false, false);
items_.emplace_back(TYPE_CHECK, "check 1", nullptr);
items_.emplace_back(TYPE_SEPARATOR, "", nullptr);
items_.emplace_back(TYPE_SUBMENU, "submenu 3", submenu_model_.get());
items_.emplace_back(TYPE_RADIO, "radio 4", nullptr);
items_.emplace_back(TYPE_ACTIONABLE_SUBMENU, "actionable 5",
actionable_submenu_model_.get());
}
RootModel(const RootModel&) = delete;
RootModel& operator=(const RootModel&) = delete;
~RootModel() override {
// Avoid that the pointer to `submenu_model_` becomes dangling.
items_.clear();
}
base::WeakPtr<ui::MenuModel> AsWeakPtr() override {
return weak_ptr_factory_.GetWeakPtr();
}
private:
std::unique_ptr<MenuModel> submenu_model_;
std::unique_ptr<MenuModel> actionable_submenu_model_;
base::WeakPtrFactory<RootModel> weak_ptr_factory_{this};
};
void CheckSubmenu(const RootModel& model,
views::MenuItemView* menu,
views::MenuModelAdapter* delegate,
int submenu_id,
size_t expected_children,
size_t submenu_model_index,
int id) {
views::MenuItemView* submenu = menu->GetMenuItemByID(submenu_id);
views::SubmenuView* subitem_container = submenu->GetSubmenu();
EXPECT_EQ(expected_children, subitem_container->children().size());
MenuModelBase* submodel =
static_cast<MenuModelBase*>(model.GetSubmenuModelAt(submenu_model_index));
EXPECT_TRUE(submodel);
for (size_t i = 0; i < subitem_container->children().size(); ++i, ++id) {
const MenuModelBase::Item& model_item = submodel->GetItemDefinition(i);
views::MenuItemView* item = menu->GetMenuItemByID(id);
if (!item) {
EXPECT_EQ(ui::MenuModel::TYPE_SEPARATOR, model_item.type);
continue;
}
// Check placement.
EXPECT_EQ(i, submenu->GetSubmenu()->GetIndexOf(item));
// Check type.
switch (model_item.type) {
case ui::MenuModel::TYPE_TITLE:
EXPECT_EQ(views::MenuItemView::Type::kTitle, item->GetType());
break;
case ui::MenuModel::TYPE_COMMAND:
EXPECT_EQ(views::MenuItemView::Type::kNormal, item->GetType());
break;
case ui::MenuModel::TYPE_CHECK:
EXPECT_EQ(views::MenuItemView::Type::kCheckbox, item->GetType());
break;
case ui::MenuModel::TYPE_RADIO:
EXPECT_EQ(views::MenuItemView::Type::kRadio, item->GetType());
break;
case ui::MenuModel::TYPE_SEPARATOR:
case ui::MenuModel::TYPE_BUTTON_ITEM:
break;
case ui::MenuModel::TYPE_SUBMENU:
EXPECT_EQ(views::MenuItemView::Type::kSubMenu, item->GetType());
break;
case ui::MenuModel::TYPE_ACTIONABLE_SUBMENU:
EXPECT_EQ(views::MenuItemView::Type::kActionableSubMenu,
item->GetType());
break;
case ui::MenuModel::TYPE_HIGHLIGHTED:
EXPECT_EQ(views::MenuItemView::Type::kHighlighted, item->GetType());
break;
}
// Check enabled state.
EXPECT_EQ(model_item.enabled, item->GetEnabled());
// Check visibility.
EXPECT_EQ(model_item.visible, item->GetVisible());
// Check alert state.
EXPECT_EQ(model_item.alerted, item->is_alerted());
// Check new feature flag.
EXPECT_EQ(model_item.new_feature, item->is_new());
// Check activation.
static_cast<views::MenuDelegate*>(delegate)->ExecuteCommand(id);
EXPECT_EQ(i, submodel->last_activation());
submodel->set_last_activation(std::nullopt);
}
}
} // namespace
namespace views {
using MenuModelAdapterTest = ViewsTestBase;
TEST_F(MenuModelAdapterTest, BasicTest) {
// Build model and adapter.
RootModel model;
views::MenuModelAdapter delegate(&model);
// Create menu. Build menu twice to check that rebuilding works properly.
auto menu_owning = std::make_unique<MenuItemView>(&delegate);
MenuItemView* menu = menu_owning.get();
MenuRunner menu_runner(std::move(menu_owning), 0);
delegate.BuildMenu(menu);
delegate.BuildMenu(menu);
EXPECT_TRUE(menu->HasSubmenu());
// Check top level menu items.
views::SubmenuView* item_container = menu->GetSubmenu();
EXPECT_EQ(6u, item_container->children().size());
int id = kRootIdBase;
for (size_t i = 0; i < item_container->children().size(); ++i, ++id) {
const MenuModelBase::Item& model_item = model.GetItemDefinition(i);
MenuItemView* item = menu->GetMenuItemByID(id);
if (!item) {
EXPECT_EQ(ui::MenuModel::TYPE_SEPARATOR, model_item.type);
continue;
}
// Check placement.
EXPECT_EQ(i, menu->GetSubmenu()->GetIndexOf(item));
// Check type.
switch (model_item.type) {
case ui::MenuModel::TYPE_TITLE:
EXPECT_EQ(views::MenuItemView::Type::kTitle, item->GetType());
break;
case ui::MenuModel::TYPE_COMMAND:
EXPECT_EQ(views::MenuItemView::Type::kNormal, item->GetType());
break;
case ui::MenuModel::TYPE_CHECK:
EXPECT_EQ(views::MenuItemView::Type::kCheckbox, item->GetType());
break;
case ui::MenuModel::TYPE_RADIO:
EXPECT_EQ(views::MenuItemView::Type::kRadio, item->GetType());
break;
case ui::MenuModel::TYPE_SEPARATOR:
case ui::MenuModel::TYPE_BUTTON_ITEM:
break;
case ui::MenuModel::TYPE_SUBMENU:
EXPECT_EQ(views::MenuItemView::Type::kSubMenu, item->GetType());
break;
case ui::MenuModel::TYPE_ACTIONABLE_SUBMENU:
EXPECT_EQ(views::MenuItemView::Type::kActionableSubMenu,
item->GetType());
break;
case ui::MenuModel::TYPE_HIGHLIGHTED:
EXPECT_EQ(views::MenuItemView::Type::kHighlighted, item->GetType());
break;
}
// Check enabled state.
EXPECT_EQ(model_item.enabled, item->GetEnabled());
// Check visibility.
EXPECT_EQ(model_item.visible, item->GetVisible());
// Check alert state.
EXPECT_EQ(model_item.alerted, item->is_alerted());
// Check new feature flag.
EXPECT_EQ(model_item.new_feature, item->is_new());
// Check activation.
static_cast<views::MenuDelegate*>(&delegate)->ExecuteCommand(id);
EXPECT_EQ(i, model.last_activation());
model.set_last_activation(std::nullopt);
}
// Check the submenu.
const int submenu_index = 3;
CheckSubmenu(model, menu, &delegate, kRootIdBase + submenu_index, 2,
submenu_index, kSubmenuIdBase);
// Check the actionable submenu.
const int actionable_submenu_index = 5;
CheckSubmenu(model, menu, &delegate, kRootIdBase + actionable_submenu_index,
2, actionable_submenu_index, kActionableSubmenuIdBase);
}
} // namespace views