blob: 4a3cd7e0c060e8265e6c8b37ddedd3cc9e9e3e21 [file] [log] [blame]
// Copyright (c) 2012 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 <stddef.h>
#include "base/run_loop.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/base/models/combobox_model.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/checkbox.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/button/radio_button.h"
#include "ui/views/controls/combobox/combobox.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/link.h"
#include "ui/views/controls/native/native_view_host.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/tabbed_pane/tabbed_pane.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/test/focus_manager_test.h"
#include "ui/views/widget/root_view.h"
#include "ui/views/widget/widget.h"
using base::ASCIIToUTF16;
namespace views {
namespace {
enum {
TOP_CHECKBOX_ID = 1, // 1
LEFT_CONTAINER_ID,
APPLE_LABEL_ID,
APPLE_TEXTFIELD_ID,
ORANGE_LABEL_ID, // 5
ORANGE_TEXTFIELD_ID,
BANANA_LABEL_ID,
BANANA_TEXTFIELD_ID,
KIWI_LABEL_ID,
KIWI_TEXTFIELD_ID, // 10
FRUIT_BUTTON_ID,
FRUIT_CHECKBOX_ID,
COMBOBOX_ID,
RIGHT_CONTAINER_ID,
ASPARAGUS_BUTTON_ID, // 15
BROCCOLI_BUTTON_ID,
CAULIFLOWER_BUTTON_ID,
INNER_CONTAINER_ID,
SCROLL_VIEW_ID,
ROSETTA_LINK_ID, // 20
STUPEUR_ET_TREMBLEMENT_LINK_ID,
DINER_GAME_LINK_ID,
RIDICULE_LINK_ID,
CLOSET_LINK_ID,
VISITING_LINK_ID, // 25
AMELIE_LINK_ID,
JOYEUX_NOEL_LINK_ID,
CAMPING_LINK_ID,
BRICE_DE_NICE_LINK_ID,
TAXI_LINK_ID, // 30
ASTERIX_LINK_ID,
OK_BUTTON_ID,
CANCEL_BUTTON_ID,
HELP_BUTTON_ID,
STYLE_CONTAINER_ID, // 35
BOLD_CHECKBOX_ID,
ITALIC_CHECKBOX_ID,
UNDERLINED_CHECKBOX_ID,
STYLE_HELP_LINK_ID,
STYLE_TEXT_EDIT_ID, // 40
SEARCH_CONTAINER_ID,
SEARCH_TEXTFIELD_ID,
SEARCH_BUTTON_ID,
HELP_LINK_ID,
THUMBNAIL_CONTAINER_ID, // 45
THUMBNAIL_STAR_ID,
THUMBNAIL_SUPER_STAR_ID,
};
class DummyComboboxModel : public ui::ComboboxModel {
public:
// Overridden from ui::ComboboxModel:
int GetItemCount() const override { return 10; }
base::string16 GetItemAt(int index) override {
return ASCIIToUTF16("Item ") + base::IntToString16(index);
}
};
// A View that can act as a pane.
class PaneView : public View, public FocusTraversable {
public:
PaneView() : focus_search_(NULL) {}
// If this method is called, this view will use GetPaneFocusTraversable to
// have this provided FocusSearch used instead of the default one, allowing
// you to trap focus within the pane.
void EnablePaneFocus(FocusSearch* focus_search) {
focus_search_ = focus_search;
}
// Overridden from View:
FocusTraversable* GetPaneFocusTraversable() override {
if (focus_search_)
return this;
else
return NULL;
}
// Overridden from FocusTraversable:
views::FocusSearch* GetFocusSearch() override { return focus_search_; }
FocusTraversable* GetFocusTraversableParent() override { return NULL; }
View* GetFocusTraversableParentView() override { return NULL; }
private:
FocusSearch* focus_search_;
};
// BorderView is a view containing a native window with its own view hierarchy.
// It is interesting to test focus traversal from a view hierarchy to an inner
// view hierarchy.
class BorderView : public NativeViewHost {
public:
explicit BorderView(View* child) : child_(child) {
DCHECK(child);
SetFocusBehavior(FocusBehavior::NEVER);
}
virtual internal::RootView* GetContentsRootView() {
return static_cast<internal::RootView*>(widget_->GetRootView());
}
FocusTraversable* GetFocusTraversable() override {
return static_cast<internal::RootView*>(widget_->GetRootView());
}
void ViewHierarchyChanged(
const ViewHierarchyChangedDetails& details) override {
NativeViewHost::ViewHierarchyChanged(details);
if (details.child == this && details.is_add) {
if (!widget_) {
widget_ = std::make_unique<Widget>();
Widget::InitParams params(Widget::InitParams::TYPE_CONTROL);
params.parent = details.parent->GetWidget()->GetNativeView();
params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
widget_->Init(params);
widget_->SetFocusTraversableParentView(this);
widget_->SetContentsView(child_);
}
// We have been added to a view hierarchy, attach the native view.
Attach(widget_->GetNativeView());
// Also update the FocusTraversable parent so the focus traversal works.
static_cast<internal::RootView*>(widget_->GetRootView())->
SetFocusTraversableParent(GetWidget()->GetFocusTraversable());
}
}
private:
View* child_;
std::unique_ptr<Widget> widget_;
DISALLOW_COPY_AND_ASSIGN(BorderView);
};
} // namespace
class FocusTraversalTest : public FocusManagerTest {
public:
~FocusTraversalTest() override;
void InitContentView() override;
protected:
FocusTraversalTest();
View* FindViewByID(int id) {
View* view = GetContentsView()->GetViewByID(id);
if (view)
return view;
if (style_tab_)
view = style_tab_->GetSelectedTabContentView()->GetViewByID(id);
if (view)
return view;
view = search_border_view_->GetContentsRootView()->GetViewByID(id);
if (view)
return view;
return NULL;
}
protected:
// Helper function to advance focus multiple times in a loop. |traversal_ids|
// is an array of view ids of length |N|. |reverse| denotes the direction in
// which focus should be advanced.
template <size_t N>
void AdvanceEntireFocusLoop(const int (&traversal_ids)[N], bool reverse) {
for (size_t i = 0; i < 3; ++i) {
for (size_t j = 0; j < N; j++) {
SCOPED_TRACE(testing::Message() << "reverse:" << reverse << " i:" << i
<< " j:" << j);
GetFocusManager()->AdvanceFocus(reverse);
View* focused_view = GetFocusManager()->GetFocusedView();
EXPECT_NE(nullptr, focused_view);
if (focused_view)
EXPECT_EQ(traversal_ids[reverse ? N - j - 1 : j], focused_view->id());
}
}
}
TabbedPane* style_tab_;
BorderView* search_border_view_;
DummyComboboxModel combobox_model_;
PaneView* left_container_;
PaneView* right_container_;
DISALLOW_COPY_AND_ASSIGN(FocusTraversalTest);
};
FocusTraversalTest::FocusTraversalTest()
: style_tab_(NULL),
search_border_view_(NULL) {
}
FocusTraversalTest::~FocusTraversalTest() {
}
void FocusTraversalTest::InitContentView() {
// Create a complicated view hierarchy with lots of control types for
// use by all of the focus traversal tests.
//
// Class name, ID, and asterisk next to focusable views:
//
// View
// Checkbox * TOP_CHECKBOX_ID
// PaneView LEFT_CONTAINER_ID
// Label APPLE_LABEL_ID
// Textfield * APPLE_TEXTFIELD_ID
// Label ORANGE_LABEL_ID
// Textfield * ORANGE_TEXTFIELD_ID
// Label BANANA_LABEL_ID
// Textfield * BANANA_TEXTFIELD_ID
// Label KIWI_LABEL_ID
// Textfield * KIWI_TEXTFIELD_ID
// NativeButton * FRUIT_BUTTON_ID
// Checkbox * FRUIT_CHECKBOX_ID
// Combobox * COMBOBOX_ID
// PaneView RIGHT_CONTAINER_ID
// RadioButton * ASPARAGUS_BUTTON_ID
// RadioButton * BROCCOLI_BUTTON_ID
// RadioButton * CAULIFLOWER_BUTTON_ID
// View INNER_CONTAINER_ID
// ScrollView SCROLL_VIEW_ID
// View
// Link * ROSETTA_LINK_ID
// Link * STUPEUR_ET_TREMBLEMENT_LINK_ID
// Link * DINER_GAME_LINK_ID
// Link * RIDICULE_LINK_ID
// Link * CLOSET_LINK_ID
// Link * VISITING_LINK_ID
// Link * AMELIE_LINK_ID
// Link * JOYEUX_NOEL_LINK_ID
// Link * CAMPING_LINK_ID
// Link * BRICE_DE_NICE_LINK_ID
// Link * TAXI_LINK_ID
// Link * ASTERIX_LINK_ID
// NativeButton * OK_BUTTON_ID
// NativeButton * CANCEL_BUTTON_ID
// NativeButton * HELP_BUTTON_ID
// TabbedPane * STYLE_CONTAINER_ID
// TabStrip
// Tab ("Style")
// Tab ("Other")
// View
// View
// Checkbox * BOLD_CHECKBOX_ID
// Checkbox * ITALIC_CHECKBOX_ID
// Checkbox * UNDERLINED_CHECKBOX_ID
// Link * STYLE_HELP_LINK_ID
// Textfield * STYLE_TEXT_EDIT_ID
// View
// BorderView SEARCH_CONTAINER_ID
// View
// Textfield * SEARCH_TEXTFIELD_ID
// NativeButton * SEARCH_BUTTON_ID
// Link * HELP_LINK_ID
// View * THUMBNAIL_CONTAINER_ID
// NativeButton * THUMBNAIL_STAR_ID
// NativeButton * THUMBNAIL_SUPER_STAR_ID
GetContentsView()->SetBackground(CreateSolidBackground(SK_ColorWHITE));
Checkbox* cb = new Checkbox(ASCIIToUTF16("This is a checkbox"));
GetContentsView()->AddChildView(cb);
// In this fast paced world, who really has time for non hard-coded layout?
cb->SetBounds(10, 10, 200, 20);
cb->set_id(TOP_CHECKBOX_ID);
left_container_ = new PaneView();
left_container_->SetBorder(CreateSolidBorder(1, SK_ColorBLACK));
left_container_->SetBackground(
CreateSolidBackground(SkColorSetRGB(240, 240, 240)));
left_container_->set_id(LEFT_CONTAINER_ID);
GetContentsView()->AddChildView(left_container_);
left_container_->SetBounds(10, 35, 250, 200);
int label_x = 5;
int label_width = 50;
int label_height = 15;
int text_field_width = 150;
int y = 10;
int gap_between_labels = 10;
Label* label = new Label(ASCIIToUTF16("Apple:"));
label->set_id(APPLE_LABEL_ID);
left_container_->AddChildView(label);
label->SetBounds(label_x, y, label_width, label_height);
Textfield* text_field = new Textfield();
text_field->set_id(APPLE_TEXTFIELD_ID);
left_container_->AddChildView(text_field);
text_field->SetBounds(label_x + label_width + 5, y,
text_field_width, label_height);
y += label_height + gap_between_labels;
label = new Label(ASCIIToUTF16("Orange:"));
label->set_id(ORANGE_LABEL_ID);
left_container_->AddChildView(label);
label->SetBounds(label_x, y, label_width, label_height);
text_field = new Textfield();
text_field->set_id(ORANGE_TEXTFIELD_ID);
left_container_->AddChildView(text_field);
text_field->SetBounds(label_x + label_width + 5, y,
text_field_width, label_height);
y += label_height + gap_between_labels;
label = new Label(ASCIIToUTF16("Banana:"));
label->set_id(BANANA_LABEL_ID);
left_container_->AddChildView(label);
label->SetBounds(label_x, y, label_width, label_height);
text_field = new Textfield();
text_field->set_id(BANANA_TEXTFIELD_ID);
left_container_->AddChildView(text_field);
text_field->SetBounds(label_x + label_width + 5, y,
text_field_width, label_height);
y += label_height + gap_between_labels;
label = new Label(ASCIIToUTF16("Kiwi:"));
label->set_id(KIWI_LABEL_ID);
left_container_->AddChildView(label);
label->SetBounds(label_x, y, label_width, label_height);
text_field = new Textfield();
text_field->set_id(KIWI_TEXTFIELD_ID);
left_container_->AddChildView(text_field);
text_field->SetBounds(label_x + label_width + 5, y,
text_field_width, label_height);
y += label_height + gap_between_labels;
LabelButton* button = MdTextButton::Create(NULL, ASCIIToUTF16("Click me"));
button->SetBounds(label_x, y + 10, 80, 30);
button->set_id(FRUIT_BUTTON_ID);
left_container_->AddChildView(button);
y += 40;
cb = new Checkbox(ASCIIToUTF16("This is another check box"));
cb->SetBounds(label_x + label_width + 5, y, 180, 20);
cb->set_id(FRUIT_CHECKBOX_ID);
left_container_->AddChildView(cb);
y += 20;
Combobox* combobox = new Combobox(&combobox_model_);
combobox->SetBounds(label_x + label_width + 5, y, 150, 30);
combobox->set_id(COMBOBOX_ID);
left_container_->AddChildView(combobox);
right_container_ = new PaneView();
right_container_->SetBorder(CreateSolidBorder(1, SK_ColorBLACK));
right_container_->SetBackground(
CreateSolidBackground(SkColorSetRGB(240, 240, 240)));
right_container_->set_id(RIGHT_CONTAINER_ID);
GetContentsView()->AddChildView(right_container_);
right_container_->SetBounds(270, 35, 300, 200);
y = 10;
int radio_button_height = 18;
int gap_between_radio_buttons = 10;
RadioButton* radio_button = new RadioButton(ASCIIToUTF16("Asparagus"), 1);
radio_button->set_id(ASPARAGUS_BUTTON_ID);
right_container_->AddChildView(radio_button);
radio_button->SetBounds(5, y, 70, radio_button_height);
radio_button->SetGroup(1);
y += radio_button_height + gap_between_radio_buttons;
radio_button = new RadioButton(ASCIIToUTF16("Broccoli"), 1);
radio_button->set_id(BROCCOLI_BUTTON_ID);
right_container_->AddChildView(radio_button);
radio_button->SetBounds(5, y, 70, radio_button_height);
radio_button->SetGroup(1);
RadioButton* radio_button_to_check = radio_button;
y += radio_button_height + gap_between_radio_buttons;
radio_button = new RadioButton(ASCIIToUTF16("Cauliflower"), 1);
radio_button->set_id(CAULIFLOWER_BUTTON_ID);
right_container_->AddChildView(radio_button);
radio_button->SetBounds(5, y, 70, radio_button_height);
radio_button->SetGroup(1);
y += radio_button_height + gap_between_radio_buttons;
View* inner_container = new View();
inner_container->SetBorder(CreateSolidBorder(1, SK_ColorBLACK));
inner_container->SetBackground(
CreateSolidBackground(SkColorSetRGB(230, 230, 230)));
inner_container->set_id(INNER_CONTAINER_ID);
right_container_->AddChildView(inner_container);
inner_container->SetBounds(100, 10, 150, 180);
ScrollView* scroll_view = new ScrollView();
scroll_view->set_id(SCROLL_VIEW_ID);
inner_container->AddChildView(scroll_view);
scroll_view->SetBounds(1, 1, 148, 178);
View* scroll_content = new View();
scroll_content->SetBounds(0, 0, 200, 200);
scroll_content->SetBackground(
CreateSolidBackground(SkColorSetRGB(200, 200, 200)));
scroll_view->SetContents(scroll_content);
static const char* const kTitles[] = {
"Rosetta", "Stupeur et tremblement", "The diner game",
"Ridicule", "Le placard", "Les Visiteurs", "Amelie",
"Joyeux Noel", "Camping", "Brice de Nice",
"Taxi", "Asterix"
};
static const int kIDs[] = {ROSETTA_LINK_ID, STUPEUR_ET_TREMBLEMENT_LINK_ID,
DINER_GAME_LINK_ID, RIDICULE_LINK_ID,
CLOSET_LINK_ID, VISITING_LINK_ID,
AMELIE_LINK_ID, JOYEUX_NOEL_LINK_ID,
CAMPING_LINK_ID, BRICE_DE_NICE_LINK_ID,
TAXI_LINK_ID, ASTERIX_LINK_ID};
DCHECK(base::size(kTitles) == base::size(kIDs));
y = 5;
for (size_t i = 0; i < base::size(kTitles); ++i) {
Link* link = new Link(ASCIIToUTF16(kTitles[i]));
link->SetHorizontalAlignment(gfx::ALIGN_LEFT);
link->set_id(kIDs[i]);
scroll_content->AddChildView(link);
link->SetBounds(5, y, 300, 15);
y += 15;
}
y = 250;
int width = 60;
button = MdTextButton::Create(NULL, ASCIIToUTF16("OK"));
button->set_id(OK_BUTTON_ID);
button->SetIsDefault(true);
GetContentsView()->AddChildView(button);
button->SetBounds(150, y, width, 30);
button = MdTextButton::Create(NULL, ASCIIToUTF16("Cancel"));
button->set_id(CANCEL_BUTTON_ID);
GetContentsView()->AddChildView(button);
button->SetBounds(220, y, width, 30);
button = MdTextButton::Create(NULL, ASCIIToUTF16("Help"));
button->set_id(HELP_BUTTON_ID);
GetContentsView()->AddChildView(button);
button->SetBounds(290, y, width, 30);
y += 40;
View* contents = NULL;
Link* link = NULL;
// Left bottom box with style checkboxes.
contents = new View();
contents->SetBackground(CreateSolidBackground(SK_ColorWHITE));
cb = new Checkbox(ASCIIToUTF16("Bold"));
contents->AddChildView(cb);
cb->SetBounds(10, 10, 50, 20);
cb->set_id(BOLD_CHECKBOX_ID);
cb = new Checkbox(ASCIIToUTF16("Italic"));
contents->AddChildView(cb);
cb->SetBounds(70, 10, 50, 20);
cb->set_id(ITALIC_CHECKBOX_ID);
cb = new Checkbox(ASCIIToUTF16("Underlined"));
contents->AddChildView(cb);
cb->SetBounds(130, 10, 70, 20);
cb->set_id(UNDERLINED_CHECKBOX_ID);
link = new Link(ASCIIToUTF16("Help"));
contents->AddChildView(link);
link->SetBounds(10, 35, 70, 10);
link->set_id(STYLE_HELP_LINK_ID);
text_field = new Textfield();
contents->AddChildView(text_field);
text_field->SetBounds(10, 50, 100, 20);
text_field->set_id(STYLE_TEXT_EDIT_ID);
style_tab_ = new TabbedPane();
GetContentsView()->AddChildView(style_tab_);
style_tab_->SetBounds(10, y, 210, 100);
style_tab_->AddTab(ASCIIToUTF16("Style"), contents);
style_tab_->GetSelectedTab()->set_id(STYLE_CONTAINER_ID);
style_tab_->AddTab(ASCIIToUTF16("Other"), new View());
// Right bottom box with search.
contents = new View();
contents->SetBackground(CreateSolidBackground(SK_ColorWHITE));
text_field = new Textfield();
contents->AddChildView(text_field);
text_field->SetBounds(10, 10, 100, 20);
text_field->set_id(SEARCH_TEXTFIELD_ID);
button = MdTextButton::Create(NULL, ASCIIToUTF16("Search"));
contents->AddChildView(button);
button->SetBounds(112, 5, 60, 30);
button->set_id(SEARCH_BUTTON_ID);
link = new Link(ASCIIToUTF16("Help"));
link->SetHorizontalAlignment(gfx::ALIGN_LEFT);
link->set_id(HELP_LINK_ID);
contents->AddChildView(link);
link->SetBounds(175, 10, 30, 20);
search_border_view_ = new BorderView(contents);
search_border_view_->set_id(SEARCH_CONTAINER_ID);
GetContentsView()->AddChildView(search_border_view_);
search_border_view_->SetBounds(300, y, 240, 50);
y += 60;
contents = new View();
contents->SetFocusBehavior(View::FocusBehavior::ALWAYS);
contents->SetBackground(CreateSolidBackground(SK_ColorBLUE));
contents->set_id(THUMBNAIL_CONTAINER_ID);
button = MdTextButton::Create(NULL, ASCIIToUTF16("Star"));
contents->AddChildView(button);
button->SetBounds(5, 5, 50, 30);
button->set_id(THUMBNAIL_STAR_ID);
button = MdTextButton::Create(NULL, ASCIIToUTF16("SuperStar"));
contents->AddChildView(button);
button->SetBounds(60, 5, 100, 30);
button->set_id(THUMBNAIL_SUPER_STAR_ID);
GetContentsView()->AddChildView(contents);
contents->SetBounds(250, y, 200, 50);
// We can only call RadioButton::SetChecked() on the radio-button is part of
// the view hierarchy.
radio_button_to_check->SetChecked(true);
}
TEST_F(FocusTraversalTest, NormalTraversal) {
const int kTraversalIDs[] = {TOP_CHECKBOX_ID,
APPLE_TEXTFIELD_ID,
ORANGE_TEXTFIELD_ID,
BANANA_TEXTFIELD_ID,
KIWI_TEXTFIELD_ID,
FRUIT_BUTTON_ID,
FRUIT_CHECKBOX_ID,
COMBOBOX_ID,
BROCCOLI_BUTTON_ID,
ROSETTA_LINK_ID,
STUPEUR_ET_TREMBLEMENT_LINK_ID,
DINER_GAME_LINK_ID,
RIDICULE_LINK_ID,
CLOSET_LINK_ID,
VISITING_LINK_ID,
AMELIE_LINK_ID,
JOYEUX_NOEL_LINK_ID,
CAMPING_LINK_ID,
BRICE_DE_NICE_LINK_ID,
TAXI_LINK_ID,
ASTERIX_LINK_ID,
OK_BUTTON_ID,
CANCEL_BUTTON_ID,
HELP_BUTTON_ID,
STYLE_CONTAINER_ID,
BOLD_CHECKBOX_ID,
ITALIC_CHECKBOX_ID,
UNDERLINED_CHECKBOX_ID,
STYLE_HELP_LINK_ID,
STYLE_TEXT_EDIT_ID,
SEARCH_TEXTFIELD_ID,
SEARCH_BUTTON_ID,
HELP_LINK_ID,
THUMBNAIL_CONTAINER_ID,
THUMBNAIL_STAR_ID,
THUMBNAIL_SUPER_STAR_ID};
SCOPED_TRACE("NormalTraversal");
// Let's traverse the whole focus hierarchy (several times, to make sure it
// loops OK).
GetFocusManager()->ClearFocus();
AdvanceEntireFocusLoop(kTraversalIDs, false);
// Let's traverse in reverse order.
GetFocusManager()->ClearFocus();
AdvanceEntireFocusLoop(kTraversalIDs, true);
}
#if defined(OS_MACOSX)
// Test focus traversal with full keyboard access off on Mac.
TEST_F(FocusTraversalTest, NormalTraversalMac) {
GetFocusManager()->SetKeyboardAccessible(false);
// Now only views with FocusBehavior of ALWAYS will be focusable.
const int kTraversalIDs[] = {APPLE_TEXTFIELD_ID, ORANGE_TEXTFIELD_ID,
BANANA_TEXTFIELD_ID, KIWI_TEXTFIELD_ID,
STYLE_TEXT_EDIT_ID, SEARCH_TEXTFIELD_ID,
THUMBNAIL_CONTAINER_ID};
SCOPED_TRACE("NormalTraversalMac");
// Let's traverse the whole focus hierarchy (several times, to make sure it
// loops OK).
GetFocusManager()->ClearFocus();
AdvanceEntireFocusLoop(kTraversalIDs, false);
// Let's traverse in reverse order.
GetFocusManager()->ClearFocus();
AdvanceEntireFocusLoop(kTraversalIDs, true);
}
// Test toggling full keyboard access correctly changes the focused view on Mac.
TEST_F(FocusTraversalTest, FullKeyboardToggle) {
// Give focus to TOP_CHECKBOX_ID .
FindViewByID(TOP_CHECKBOX_ID)->RequestFocus();
EXPECT_EQ(TOP_CHECKBOX_ID, GetFocusManager()->GetFocusedView()->id());
// Turn off full keyboard access. Focus should move to next view with ALWAYS
// focus behavior.
GetFocusManager()->SetKeyboardAccessible(false);
EXPECT_EQ(APPLE_TEXTFIELD_ID, GetFocusManager()->GetFocusedView()->id());
// Turning on full keyboard access should not change the focused view.
GetFocusManager()->SetKeyboardAccessible(true);
EXPECT_EQ(APPLE_TEXTFIELD_ID, GetFocusManager()->GetFocusedView()->id());
// Give focus to SEARCH_BUTTON_ID.
FindViewByID(SEARCH_BUTTON_ID)->RequestFocus();
EXPECT_EQ(SEARCH_BUTTON_ID, GetFocusManager()->GetFocusedView()->id());
// Turn off full keyboard access. Focus should move to next view with ALWAYS
// focus behavior.
GetFocusManager()->SetKeyboardAccessible(false);
EXPECT_EQ(THUMBNAIL_CONTAINER_ID, GetFocusManager()->GetFocusedView()->id());
// See focus advances correctly in both directions.
GetFocusManager()->AdvanceFocus(false);
EXPECT_EQ(APPLE_TEXTFIELD_ID, GetFocusManager()->GetFocusedView()->id());
GetFocusManager()->AdvanceFocus(true);
EXPECT_EQ(THUMBNAIL_CONTAINER_ID, GetFocusManager()->GetFocusedView()->id());
}
#endif // OS_MACOSX
TEST_F(FocusTraversalTest, TraversalWithNonEnabledViews) {
const int kDisabledIDs[] = {
BANANA_TEXTFIELD_ID, FRUIT_CHECKBOX_ID, COMBOBOX_ID,
ASPARAGUS_BUTTON_ID, CAULIFLOWER_BUTTON_ID, CLOSET_LINK_ID,
VISITING_LINK_ID, BRICE_DE_NICE_LINK_ID, TAXI_LINK_ID,
ASTERIX_LINK_ID, HELP_BUTTON_ID, BOLD_CHECKBOX_ID,
SEARCH_TEXTFIELD_ID, HELP_LINK_ID};
const int kTraversalIDs[] = {
TOP_CHECKBOX_ID, APPLE_TEXTFIELD_ID,
ORANGE_TEXTFIELD_ID, KIWI_TEXTFIELD_ID,
FRUIT_BUTTON_ID, BROCCOLI_BUTTON_ID,
ROSETTA_LINK_ID, STUPEUR_ET_TREMBLEMENT_LINK_ID,
DINER_GAME_LINK_ID, RIDICULE_LINK_ID,
AMELIE_LINK_ID, JOYEUX_NOEL_LINK_ID,
CAMPING_LINK_ID, OK_BUTTON_ID,
CANCEL_BUTTON_ID, STYLE_CONTAINER_ID,
ITALIC_CHECKBOX_ID, UNDERLINED_CHECKBOX_ID,
STYLE_HELP_LINK_ID, STYLE_TEXT_EDIT_ID,
SEARCH_BUTTON_ID, THUMBNAIL_CONTAINER_ID,
THUMBNAIL_STAR_ID, THUMBNAIL_SUPER_STAR_ID};
SCOPED_TRACE("TraversalWithNonEnabledViews");
// Let's disable some views.
for (size_t i = 0; i < base::size(kDisabledIDs); i++) {
View* v = FindViewByID(kDisabledIDs[i]);
ASSERT_TRUE(v != NULL);
v->SetEnabled(false);
}
// Let's do one traversal (several times, to make sure it loops ok).
GetFocusManager()->ClearFocus();
AdvanceEntireFocusLoop(kTraversalIDs, false);
// Same thing in reverse.
GetFocusManager()->ClearFocus();
AdvanceEntireFocusLoop(kTraversalIDs, true);
}
TEST_F(FocusTraversalTest, TraversalWithInvisibleViews) {
const int kInvisibleIDs[] = {TOP_CHECKBOX_ID, OK_BUTTON_ID,
THUMBNAIL_CONTAINER_ID};
const int kTraversalIDs[] = {
APPLE_TEXTFIELD_ID, ORANGE_TEXTFIELD_ID,
BANANA_TEXTFIELD_ID, KIWI_TEXTFIELD_ID,
FRUIT_BUTTON_ID, FRUIT_CHECKBOX_ID,
COMBOBOX_ID, BROCCOLI_BUTTON_ID,
ROSETTA_LINK_ID, STUPEUR_ET_TREMBLEMENT_LINK_ID,
DINER_GAME_LINK_ID, RIDICULE_LINK_ID,
CLOSET_LINK_ID, VISITING_LINK_ID,
AMELIE_LINK_ID, JOYEUX_NOEL_LINK_ID,
CAMPING_LINK_ID, BRICE_DE_NICE_LINK_ID,
TAXI_LINK_ID, ASTERIX_LINK_ID,
CANCEL_BUTTON_ID, HELP_BUTTON_ID,
STYLE_CONTAINER_ID, BOLD_CHECKBOX_ID,
ITALIC_CHECKBOX_ID, UNDERLINED_CHECKBOX_ID,
STYLE_HELP_LINK_ID, STYLE_TEXT_EDIT_ID,
SEARCH_TEXTFIELD_ID, SEARCH_BUTTON_ID,
HELP_LINK_ID};
SCOPED_TRACE("TraversalWithInvisibleViews");
// Let's make some views invisible.
for (size_t i = 0; i < base::size(kInvisibleIDs); i++) {
View* v = FindViewByID(kInvisibleIDs[i]);
ASSERT_TRUE(v != NULL);
v->SetVisible(false);
}
// Let's do one traversal (several times, to make sure it loops ok).
GetFocusManager()->ClearFocus();
AdvanceEntireFocusLoop(kTraversalIDs, false);
// Same thing in reverse.
GetFocusManager()->ClearFocus();
AdvanceEntireFocusLoop(kTraversalIDs, true);
}
TEST_F(FocusTraversalTest, PaneTraversal) {
// Tests trapping the traversal within a pane - useful for full
// keyboard accessibility for toolbars.
// First test the left container.
const int kLeftTraversalIDs[] = {APPLE_TEXTFIELD_ID, ORANGE_TEXTFIELD_ID,
BANANA_TEXTFIELD_ID, KIWI_TEXTFIELD_ID,
FRUIT_BUTTON_ID, FRUIT_CHECKBOX_ID,
COMBOBOX_ID};
SCOPED_TRACE("PaneTraversal");
FocusSearch focus_search_left(left_container_, true, false);
left_container_->EnablePaneFocus(&focus_search_left);
FindViewByID(COMBOBOX_ID)->RequestFocus();
// Traverse the focus hierarchy within the pane several times.
AdvanceEntireFocusLoop(kLeftTraversalIDs, false);
// Traverse in reverse order.
FindViewByID(APPLE_TEXTFIELD_ID)->RequestFocus();
AdvanceEntireFocusLoop(kLeftTraversalIDs, true);
// Now test the right container, but this time with accessibility mode.
// Make some links not focusable, but mark one of them as
// "accessibility focusable", so it should show up in the traversal.
const int kRightTraversalIDs[] = {
BROCCOLI_BUTTON_ID, DINER_GAME_LINK_ID, RIDICULE_LINK_ID,
CLOSET_LINK_ID, VISITING_LINK_ID, AMELIE_LINK_ID,
JOYEUX_NOEL_LINK_ID, CAMPING_LINK_ID, BRICE_DE_NICE_LINK_ID,
TAXI_LINK_ID, ASTERIX_LINK_ID};
FocusSearch focus_search_right(right_container_, true, true);
right_container_->EnablePaneFocus(&focus_search_right);
FindViewByID(ROSETTA_LINK_ID)->SetFocusBehavior(View::FocusBehavior::NEVER);
FindViewByID(STUPEUR_ET_TREMBLEMENT_LINK_ID)
->SetFocusBehavior(View::FocusBehavior::NEVER);
FindViewByID(DINER_GAME_LINK_ID)
->SetFocusBehavior(View::FocusBehavior::ACCESSIBLE_ONLY);
FindViewByID(ASTERIX_LINK_ID)->RequestFocus();
// Traverse the focus hierarchy within the pane several times.
AdvanceEntireFocusLoop(kRightTraversalIDs, false);
// Traverse in reverse order.
FindViewByID(BROCCOLI_BUTTON_ID)->RequestFocus();
AdvanceEntireFocusLoop(kRightTraversalIDs, true);
}
class FocusTraversalNonFocusableTest : public FocusManagerTest {
public:
~FocusTraversalNonFocusableTest() override {}
void InitContentView() override;
protected:
FocusTraversalNonFocusableTest() {}
private:
DISALLOW_COPY_AND_ASSIGN(FocusTraversalNonFocusableTest);
};
void FocusTraversalNonFocusableTest::InitContentView() {
// Create a complex nested view hierarchy with no focusable views. This is a
// regression test for http://crbug.com/453699. There was previously a bug
// where advancing focus backwards through this tree resulted in an
// exponential number of nodes being searched. (Each time it traverses one of
// the x1-x3-x2 triangles, it will traverse the left sibling of x1, (x+1)0,
// twice, which means it will visit O(2^n) nodes.)
//
// | 0 |
// | / \ |
// | / \ |
// | 10 1 |
// | / \ / \ |
// | / \ / \ |
// | 20 11 2 3 |
// | / \ / \ |
// | / \ / \ |
// | ... 21 12 13 |
// | / \ |
// | / \ |
// | 22 23 |
View* v = GetContentsView();
// Create 30 groups of 4 nodes. |v| is the top of each group.
for (int i = 0; i < 300; i += 10) {
// |v|'s left child is the top of the next group. If |v| is 20, this is 30.
View* v10 = new View;
v10->set_id(i + 10);
v->AddChildView(v10);
// |v|'s right child. If |v| is 20, this is 21.
View* v1 = new View;
v1->set_id(i + 1);
v->AddChildView(v1);
// |v|'s right child has two children. If |v| is 20, these are 22 and 23.
View* v2 = new View;
v2->set_id(i + 2);
View* v3 = new View;
v3->set_id(i + 3);
v1->AddChildView(v2);
v1->AddChildView(v3);
v = v10;
}
}
// See explanation in InitContentView.
// NOTE: The failure mode of this test (if http://crbug.com/453699 were to
// regress) is a timeout, due to exponential run time.
TEST_F(FocusTraversalNonFocusableTest, PathologicalSiblingTraversal) {
// Advance forwards from the root node.
GetFocusManager()->ClearFocus();
GetFocusManager()->AdvanceFocus(false);
EXPECT_FALSE(GetFocusManager()->GetFocusedView());
// Advance backwards from the root node.
GetFocusManager()->ClearFocus();
GetFocusManager()->AdvanceFocus(true);
EXPECT_FALSE(GetFocusManager()->GetFocusedView());
}
} // namespace views