#include "ui/views/focus/focus_search.h"
#include "base/logging.h"
#include "build/build_config.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/dialog_delegate.h"
namespace views {
namespace {
DialogDelegate* GetAnchoredDialog(View* view) {
DialogDelegate* dialog_delegate = view->GetProperty(kAnchoredDialogKey);
if (dialog_delegate && dialog_delegate->GetWidget() &&
dialog_delegate->GetWidget()->IsVisible()) {
return dialog_delegate;
}
return nullptr;
}
}
FocusSearch::FocusSearch(View* root, bool cycle, bool accessibility_mode)
: root_(root), cycle_(cycle), accessibility_mode_(accessibility_mode) {
#if BUILDFLAG(IS_MAC)
accessibility_mode_ = false;
#endif
}
View* FocusSearch::FindNextFocusableView(
View* starting_view,
FocusSearch::SearchDirection search_direction,
FocusSearch::TraversalDirection traversal_direction,
FocusSearch::StartingViewPolicy check_starting_view,
FocusSearch::AnchoredDialogPolicy can_go_into_anchored_dialog,
FocusTraversable** focus_traversable,
View** focus_traversable_view) {
DCHECK(!root_->children().empty());
*focus_traversable = nullptr;
*focus_traversable_view = nullptr;
View* initial_starting_view = starting_view;
int starting_view_group = -1;
if (starting_view) {
starting_view_group = starting_view->GetGroup();
}
if (!starting_view) {
starting_view = search_direction == SearchDirection::kBackwards
? root_->children().back()
: root_->children().front();
check_starting_view = StartingViewPolicy::kCheckStartingView;
} else {
DCHECK(Contains(root_, starting_view));
}
base::flat_set<View*> seen_views;
View* v = nullptr;
if (search_direction == SearchDirection::kForwards) {
v = FindNextFocusableViewImpl(
starting_view, check_starting_view, true,
(traversal_direction == TraversalDirection::kDown),
can_go_into_anchored_dialog, starting_view_group, &seen_views,
focus_traversable, focus_traversable_view);
} else {
bool can_go_down = (traversal_direction == TraversalDirection::kDown) &&
!IsFocusable(starting_view);
v = FindPreviousFocusableViewImpl(
starting_view, check_starting_view, true, can_go_down,
can_go_into_anchored_dialog, starting_view_group, &seen_views,
focus_traversable, focus_traversable_view);
}
if (v && v != root_ && !Contains(root_, v)) {
v = nullptr;
}
if (*focus_traversable) {
DCHECK(*focus_traversable_view);
DCHECK_EQ(v, nullptr);
return nullptr;
}
if (cycle_ && !v && initial_starting_view) {
v = FindNextFocusableView(nullptr, search_direction, traversal_direction,
check_starting_view, can_go_into_anchored_dialog,
focus_traversable, focus_traversable_view);
}
if (v) {
DCHECK(IsFocusable(v));
return v;
}
return nullptr;
}
bool FocusSearch::IsViewFocusableCandidate(View* v, int skip_group_id) {
return IsFocusable(v) &&
(v->IsGroupFocusTraversable() || skip_group_id == -1 ||
v->GetGroup() != skip_group_id);
}
bool FocusSearch::IsFocusable(View* v) {
DCHECK(root_);
DCHECK(!(accessibility_mode_ &&
root_->GetWidget()->GetFocusManager()->keyboard_accessible()));
if (accessibility_mode_ ||
root_->GetWidget()->GetFocusManager()->keyboard_accessible()) {
return v && v->GetViewAccessibility().IsAccessibilityFocusable();
}
return v && v->IsFocusable();
}
View* FocusSearch::FindSelectedViewForGroup(View* view) {
if (view->IsGroupFocusTraversable() ||
view->GetGroup() == -1) {
return view;
}
View* selected_view = view->GetSelectedViewForGroup(view->GetGroup());
if (selected_view) {
return selected_view;
}
return view;
}
View* FocusSearch::GetParent(View* v) {
return Contains(root_, v) ? v->parent() : nullptr;
}
bool FocusSearch::Contains(View* root, const View* v) {
return root->Contains(v);
}
View* FocusSearch::FindNextFocusableViewImpl(
View* starting_view,
FocusSearch::StartingViewPolicy check_starting_view,
bool can_go_up,
bool can_go_down,
AnchoredDialogPolicy can_go_into_anchored_dialog,
int skip_group_id,
base::flat_set<View*>* seen_views,
FocusTraversable** focus_traversable,
View** focus_traversable_view) {
if (seen_views->contains(starting_view)) {
LOG(ERROR) << "View focus cycle detected.";
return nullptr;
}
seen_views->insert(starting_view);
if (check_starting_view == StartingViewPolicy::kCheckStartingView) {
if (IsViewFocusableCandidate(starting_view, skip_group_id)) {
View* v = FindSelectedViewForGroup(starting_view);
if (IsFocusable(v)) {
return v;
}
}
*focus_traversable = starting_view->GetFocusTraversable();
if (*focus_traversable) {
*focus_traversable_view = starting_view;
return nullptr;
}
}
if (can_go_down) {
if (!starting_view->children().empty()) {
View* view = starting_view->GetChildrenFocusList().front();
View* v = FindNextFocusableViewImpl(
view, StartingViewPolicy::kCheckStartingView, false, true,
can_go_into_anchored_dialog, skip_group_id, seen_views,
focus_traversable, focus_traversable_view);
if (v || *focus_traversable) {
return v;
}
}
if (can_go_into_anchored_dialog ==
AnchoredDialogPolicy::kCanGoIntoAnchoredDialog) {
DialogDelegate* bubble = GetAnchoredDialog(starting_view);
if (bubble) {
*focus_traversable = bubble->GetWidget()->GetFocusTraversable();
*focus_traversable_view = starting_view;
return nullptr;
}
}
}
View* sibling = starting_view->GetNextFocusableView();
if (sibling) {
View* v = FindNextFocusableViewImpl(
sibling, FocusSearch::StartingViewPolicy::kCheckStartingView, false,
true, can_go_into_anchored_dialog, skip_group_id, seen_views,
focus_traversable, focus_traversable_view);
if (v || *focus_traversable) {
return v;
}
}
if (can_go_up) {
View* parent = GetParent(starting_view);
while (parent && parent != root_) {
if (can_go_into_anchored_dialog ==
AnchoredDialogPolicy::kCanGoIntoAnchoredDialog) {
DialogDelegate* bubble = GetAnchoredDialog(parent);
if (bubble) {
*focus_traversable = bubble->GetWidget()->GetFocusTraversable();
*focus_traversable_view = starting_view;
return nullptr;
}
}
sibling = parent->GetNextFocusableView();
if (sibling) {
return FindNextFocusableViewImpl(
sibling, StartingViewPolicy::kCheckStartingView, true, true,
can_go_into_anchored_dialog, skip_group_id, seen_views,
focus_traversable, focus_traversable_view);
}
parent = GetParent(parent);
}
}
return nullptr;
}
View* FocusSearch::FindPreviousFocusableViewImpl(
View* starting_view,
FocusSearch::StartingViewPolicy check_starting_view,
bool can_go_up,
bool can_go_down,
FocusSearch::AnchoredDialogPolicy can_go_into_anchored_dialog,
int skip_group_id,
base::flat_set<View*>* seen_views,
FocusTraversable** focus_traversable,
View** focus_traversable_view) {
if (seen_views->contains(starting_view)) {
LOG(ERROR) << "View focus cycle detected.";
return nullptr;
}
seen_views->insert(starting_view);
if (starting_view->GetProperty(kAnchoredDialogKey) &&
can_go_into_anchored_dialog ==
AnchoredDialogPolicy::kSkipAnchoredDialog &&
!can_go_down) {
can_go_down = true;
}
if (can_go_down) {
*focus_traversable = starting_view->GetFocusTraversable();
if (*focus_traversable) {
*focus_traversable_view = starting_view;
return nullptr;
}
if (can_go_into_anchored_dialog ==
AnchoredDialogPolicy::kCanGoIntoAnchoredDialog) {
DialogDelegate* bubble = GetAnchoredDialog(starting_view);
if (bubble) {
*focus_traversable = bubble->GetWidget()->GetFocusTraversable();
*focus_traversable_view = starting_view;
return nullptr;
}
}
can_go_into_anchored_dialog =
AnchoredDialogPolicy::kCanGoIntoAnchoredDialog;
if (!starting_view->children().empty()) {
View* view = starting_view->GetChildrenFocusList().back();
View* v = FindPreviousFocusableViewImpl(
view, StartingViewPolicy::kCheckStartingView, false, true,
can_go_into_anchored_dialog, skip_group_id, seen_views,
focus_traversable, focus_traversable_view);
if (v || *focus_traversable) {
return v;
}
}
}
if (check_starting_view == StartingViewPolicy::kCheckStartingView &&
IsViewFocusableCandidate(starting_view, skip_group_id)) {
View* v = FindSelectedViewForGroup(starting_view);
if (IsFocusable(v)) {
return v;
}
}
View* sibling = starting_view->GetPreviousFocusableView();
if (sibling) {
return FindPreviousFocusableViewImpl(
sibling, StartingViewPolicy::kCheckStartingView, can_go_up, true,
can_go_into_anchored_dialog, skip_group_id, seen_views,
focus_traversable, focus_traversable_view);
}
if (can_go_up) {
View* parent = GetParent(starting_view);
if (parent) {
return FindPreviousFocusableViewImpl(
parent, StartingViewPolicy::kCheckStartingView, true, false,
can_go_into_anchored_dialog, skip_group_id, seen_views,
focus_traversable, focus_traversable_view);
}
}
return nullptr;
}
}