1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
ash / wm / overview / overview_focus_cycler.cc [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#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 {
// Returns true if any child is focusable in `view`'s tree.
bool IsViewFocusable(const views::View* view, bool for_accessibility) {
CHECK(view);
// A regular focusable view is focusable for ChromeVox as well.
if (view->IsFocusable()) {
return true;
}
if (for_accessibility &&
view->GetViewAccessibility().IsAccessibilityFocusable()) {
return true;
}
// If any of the children are focusable we are done.
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(
/*starting_view=*/nullptr, widget, reverse, /*dont_loop=*/false);
CHECK(view);
return view;
}
// Determines whether we should rotate focus to the next widget. We rotate focus
// if we are forward tabbing and the current focused view is the last focusable
// view of the widget, or if we are reverse tabbing and the current focused view
// is the first focusable view of the widget.
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 that temporary makes a widget activatable so that we can focus it. This
// is meant to be used while keyboard traversing through overview item widgets.
// These widgets are not activatable normally for both historical reasons, and
// to prevent activation change while mouse dragging.
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;
}
// If an overview item received focus, we need to restack the original
// window above the overview item widget, otherwise the overview backdrop
// would end up covering the original window.
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};
};
} // namespace
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)) {
// If we don't need to rotate focus to the next widget, let the focus
// manager advance focus.
focused_view->GetWidget()->GetFocusManager()->AdvanceFocus(reverse);
return;
}
const std::vector<views::Widget*> widgets =
GetTraversableWidgets(/*for_accessibility=*/false);
// `widgets` can be empty when there are only non traversable overview widgets
// shown (ex. "No recent items" label).
if (widgets.empty()) {
return;
}
// If there is no current focused view request either the last focusable view
// of the last widget in the traversal or the first focusable view of the
// first widget, depending on `reverse`.
if (!focused_view) {
views::Widget* widget = reverse ? widgets.back() : widgets.front();
ScopedActivatable scoped_activatable(widget);
GetFirstOrLastFocusableView(widget, reverse)->RequestFocus();
return;
}
auto it = base::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());
// Jump to the desk removal toast if it exists. We introduce special logic
// here since it's not an overview UI.
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;
}
}
// Focus the last focusable view of the previous widget if `reverse`, or the
// first focusable view of the next widget otherwise.
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(/*for_accessibility=*/true);
if (a11y_widgets.empty()) {
return;
}
auto get_view_a11y = [&a11y_widgets](int index) -> views::ViewAccessibility& {
return a11y_widgets[index]->GetContentsView()->GetViewAccessibility();
};
// If there is only one widget left, clear the focus overrides so that they
// do not point to deleted objects.
if (a11y_widgets.size() == 1) {
get_view_a11y(/*index=*/0).SetPreviousFocus(nullptr);
get_view_a11y(/*index=*/0).SetNextFocus(nullptr);
a11y_widgets[0]->GetContentsView()->NotifyAccessibilityEvent(
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()->NotifyAccessibilityEvent(
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); // Conservative default.
auto maybe_add_widget = [for_accessibility,
&traversable_widgets](views::Widget* widget) {
// The desks bar is invisible when in splitscreen. This is because of the
// high cost of creating and destroying it.
if (!widget || !widget->IsVisible() ||
widget->GetNativeWindow()->layer()->GetTargetOpacity() == 0.f) {
return;
}
// Focus is tied to activation except in ChromeVox where labels and other
// normally unfocusable elements can be ChromeVox focused.
if (!for_accessibility && !widget->CanActivate() &&
!widget->GetNativeWindow()->GetProperty(kIsOverviewItemKey)) {
return;
}
// Skip this widget if it has no focusable views. (i.e. Saved desks library
// with all saved desks deleted or saved desk button container with all
// buttons disabled.)
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()) {
// There may be two widgets if the item is a snap group item.
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;
}
} // namespace ash