#include "ash/wm/overview/overview_focus_cycler.h"
#include "ash/shell.h"
#include "ash/style/rounded_label_widget.h"
#include "ash/wm/desks/desk_preview_view.h"
#include "ash/wm/overview/overview_grid.h"
#include "ash/wm/overview/overview_item_view.h"
#include "ash/wm/overview/overview_session.h"
#include "ash/wm/window_properties.h"
#include "ash/wm/window_util.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/views/widget/widget_observer.h"
namespace ash {
namespace {
bool IsViewFocusable(const views::View* view, bool for_accessibility) {
CHECK(view);
if (view->IsFocusable()) {
return true;
}
if (for_accessibility &&
view->GetViewAccessibility().IsAccessibilityFocusable()) {
return true;
}
for (const views::View* child : view->children()) {
if (IsViewFocusable(child, for_accessibility)) {
return true;
}
}
return false;
}
views::View* GetFirstOrLastFocusableView(views::Widget* widget, bool reverse) {
views::View* view = widget->GetFocusManager()->GetNextFocusableView(
nullptr, widget, reverse, false);
CHECK(view);
return view;
}
bool ShouldRotateFocus(views::View* current_focused_view, bool reverse) {
views::Widget* widget = current_focused_view->GetWidget();
return current_focused_view == GetFirstOrLastFocusableView(widget, !reverse);
}
int AdvanceIndex(int previous_index, int size, bool reverse) {
if (reverse) {
return previous_index == 0 ? (size - 1) : (previous_index - 1);
}
return previous_index == (size - 1) ? 0 : (previous_index + 1);
}
class ScopedActivatable : public views::WidgetObserver {
public:
explicit ScopedActivatable(views::Widget* widget) {
views::WidgetDelegate* delegate = widget->widget_delegate();
if (!delegate->CanActivate()) {
observation_.Observe(widget);
delegate->SetCanActivate(true);
}
}
ScopedActivatable(const ScopedActivatable&) = delete;
ScopedActivatable& operator=(const ScopedActivatable&) = delete;
~ScopedActivatable() override {
if (observation_.IsObserving()) {
observation_.GetSource()->widget_delegate()->SetCanActivate(false);
}
}
void OnWidgetDestroying(views::Widget* widget) override {
observation_.Reset();
}
void OnWidgetActivationChanged(views::Widget* widget, bool active) override {
if (!active) {
return;
}
auto* item_view =
views::AsViewClass<OverviewItemView>(widget->GetContentsView());
if (!item_view) {
return;
}
OverviewItemBase* item = item_view->overview_item();
if (!item) {
return;
}
aura::Window* parent = widget->GetNativeWindow()->parent();
if (parent == item->GetWindow()->parent()) {
parent->StackChildAbove(item->GetWindow(), widget->GetNativeWindow());
}
}
private:
base::ScopedObservation<views::Widget, views::WidgetObserver> observation_{
this};
};
}
OverviewFocusCycler::OverviewFocusCycler(OverviewSession* overview_session)
: overview_session_(overview_session) {}
OverviewFocusCycler::~OverviewFocusCycler() = default;
void OverviewFocusCycler::MoveFocus(bool reverse) {
views::View* focused_view = GetOverviewFocusedView();
if (focused_view && !ShouldRotateFocus(focused_view, reverse)) {
focused_view->GetWidget()->GetFocusManager()->AdvanceFocus(reverse);
return;
}
const std::vector<views::Widget*> widgets =
GetTraversableWidgets(false);
if (widgets.empty()) {
return;
}
if (!focused_view) {
views::Widget* widget = reverse ? widgets.back() : widgets.front();
ScopedActivatable scoped_activatable(widget);
GetFirstOrLastFocusableView(widget, reverse)->RequestFocus();
return;
}
auto it = std::ranges::find(widgets, focused_view->GetWidget());
CHECK(it != widgets.end());
const int previous_index = std::distance(widgets.begin(), it);
const int size = static_cast<int>(widgets.size());
if ((reverse && previous_index == 0) ||
(!reverse && previous_index == size - 1)) {
const bool ignore_activations = overview_session_->ignore_activations();
overview_session_->set_ignore_activations(true);
const bool focused_toast =
DesksController::Get()->RequestFocusOnUndoDeskRemovalToast();
overview_session_->set_ignore_activations(ignore_activations);
if (focused_toast) {
return;
}
}
const int next_index = AdvanceIndex(previous_index, size, reverse);
ScopedActivatable scoped_activatable(widgets[next_index]);
GetFirstOrLastFocusableView(widgets[next_index], reverse)->RequestFocus();
}
bool OverviewFocusCycler::AcceptSelection() {
views::View* focused_view = GetOverviewFocusedView();
if (!focused_view) {
return false;
}
if (auto* preview_view = views::AsViewClass<DeskPreviewView>(focused_view)) {
preview_view->AcceptSelection();
return true;
}
if (auto* item_view = views::AsViewClass<OverviewItemView>(focused_view)) {
item_view->AcceptSelection(overview_session_);
return true;
}
return false;
}
views::View* OverviewFocusCycler::GetOverviewFocusedView() {
aura::Window* active_window = window_util::GetActiveWindow();
if (!active_window) {
return nullptr;
}
if (!active_window->GetProperty(kOverviewUiKey)) {
return nullptr;
}
views::Widget* widget =
views::Widget::GetWidgetForNativeWindow(active_window);
if (!widget) {
return nullptr;
}
return widget->GetFocusManager()->GetFocusedView();
}
void OverviewFocusCycler::UpdateAccessibilityFocus() {
const std::vector<views::Widget*> a11y_widgets =
GetTraversableWidgets(true);
if (a11y_widgets.empty()) {
return;
}
auto get_view_a11y = [&a11y_widgets](int index) -> views::ViewAccessibility& {
return a11y_widgets[index]->GetContentsView()->GetViewAccessibility();
};
if (a11y_widgets.size() == 1) {
get_view_a11y(0).SetPreviousFocus(nullptr);
get_view_a11y(0).SetNextFocus(nullptr);
a11y_widgets[0]->GetContentsView()->NotifyAccessibilityEventDeprecated(
ax::mojom::Event::kTreeChanged, true);
return;
}
int size = a11y_widgets.size();
for (int i = 0; i < size; ++i) {
int previous_index = (i + size - 1) % size;
int next_index = (i + 1) % size;
get_view_a11y(i).SetPreviousFocus(a11y_widgets[previous_index]);
get_view_a11y(i).SetNextFocus(a11y_widgets[next_index]);
a11y_widgets[i]->GetContentsView()->NotifyAccessibilityEventDeprecated(
ax::mojom::Event::kTreeChanged, true);
}
}
std::vector<views::Widget*> OverviewFocusCycler::GetTraversableWidgets(
bool for_accessibility) const {
std::vector<views::Widget*> traversable_widgets;
traversable_widgets.reserve(40);
auto maybe_add_widget = [for_accessibility,
&traversable_widgets](views::Widget* widget) {
if (!widget || !widget->IsVisible() ||
widget->GetNativeWindow()->layer()->GetTargetOpacity() == 0.f) {
return;
}
if (!for_accessibility && !widget->CanActivate() &&
!widget->GetNativeWindow()->GetProperty(kIsOverviewItemKey)) {
return;
}
if (!IsViewFocusable(widget->GetContentsView(), for_accessibility)) {
return;
}
traversable_widgets.push_back(widget);
};
maybe_add_widget(overview_session_->overview_focus_widget());
for (const auto& grid : overview_session_->grid_list()) {
for (const auto& item : grid->item_list()) {
for (views::Widget* item_widget : item->GetFocusableWidgets()) {
maybe_add_widget(item_widget);
}
}
maybe_add_widget(grid->saved_desk_library_widget());
maybe_add_widget(grid->desks_widget());
maybe_add_widget(grid->save_desk_button_container_widget());
maybe_add_widget(grid->informed_restore_widget());
maybe_add_widget(grid->birch_bar_widget());
maybe_add_widget(grid->split_view_setup_widget());
maybe_add_widget(grid->no_windows_widget());
}
return traversable_widgets;
}
}