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
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
ash / wm / window_cycle / window_cycle_controller.cc [blame]
// Copyright 2014 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/window_cycle/window_cycle_controller.h"
#include "ash/accessibility/accessibility_controller.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/events/event_rewriter_controller_impl.h"
#include "ash/metrics/task_switch_metrics_recorder.h"
#include "ash/metrics/task_switch_source.h"
#include "ash/metrics/user_metrics_recorder.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/wallpaper/wallpaper_controller_impl.h"
#include "ash/wm/desks/desk.h"
#include "ash/wm/desks/desk_bar_controller.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/screen_pinning_controller.h"
#include "ash/wm/snap_group/snap_group.h"
#include "ash/wm/snap_group/snap_group_controller.h"
#include "ash/wm/window_cycle/window_cycle_event_filter.h"
#include "ash/wm/window_cycle/window_cycle_list.h"
#include "ash/wm/window_util.h"
#include "base/check.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/user_metrics.h"
#include "components/prefs/pref_service.h"
#include "ui/base/l10n/l10n_util.h"
namespace ash {
namespace {
constexpr char kAltTabDesksSwitchDistanceHistogramName[] =
"Ash.WindowCycleController.DesksSwitchDistance";
constexpr char kAltTabInitialModeHistogramName[] =
"Ash.WindowCycleController.InitialMode";
constexpr char kAltTabItemsHistogramName[] = "Ash.WindowCycleController.Items";
constexpr char kAltTabSwitchModeHistogramName[] =
"Ash.WindowCycleController.SwitchMode";
constexpr char kAltTabModeSwitchSourceHistogramName[] =
"Ash.WindowCycleController.ModeSwitchSource";
constexpr char kSameAppWindowCycleIsSameAppHistogramName[] =
"Ash.WindowCycleController.SameApp.IsSameApp";
constexpr char kSameAppWindowCycleDeskModeHistogramName[] =
"Ash.WindowCycleController.SameApp.DeskMode";
// Enumeration of the alt-tab modes to record initial mode and mode switch.
// Note that these values are persisted to histograms so existing values should
// remain unchanged and new values should be added to the end.
enum class AltTabMode {
// The window list includes all windows from all desks.
kAllDesks,
// The window list only includes windows from the active desk.
kCurrentDesk,
kMaxValue = kCurrentDesk,
};
// Returns the most recently active window from the |window_list| or nullptr
// if the list is empty.
aura::Window* GetActiveWindow(
const WindowCycleController::WindowList& window_list) {
return window_list.empty() ? nullptr : window_list[0];
}
void ReportPossibleDesksSwitchStats(int active_desk_container_id_before_cycle) {
// Report only for users who have 2 or more desks, since we're only interested
// in seeing how users of Virtual Desks use window cycling.
auto* desks_controller = DesksController::Get();
if (!desks_controller)
return;
if (desks_controller->desks().size() < 2)
return;
// Note that this functions is called while a potential desk switch animation
// is starting, in this case we want the target active desk (i.e. the soon-to-
// be active desk after the animation finishes).
const int active_desk_container_id_after_cycle =
desks_controller->GetTargetActiveDesk()->container_id();
DCHECK_NE(active_desk_container_id_before_cycle, kShellWindowId_Invalid);
DCHECK_NE(active_desk_container_id_after_cycle, kShellWindowId_Invalid);
// Note that the desks containers IDs are consecutive. See
// |ash::ShellWindowId|.
const int desks_switch_distance =
std::abs(active_desk_container_id_after_cycle -
active_desk_container_id_before_cycle);
base::UmaHistogramExactLinear(kAltTabDesksSwitchDistanceHistogramName,
desks_switch_distance,
desks_util::kDesksUpperLimit);
}
} // namespace
//////////////////////////////////////////////////////////////////////////////
// WindowCycleController, public:
WindowCycleController::WindowCycleController() {
Shell::Get()->session_controller()->AddObserver(this);
}
WindowCycleController::~WindowCycleController() {
Shell::Get()->session_controller()->RemoveObserver(this);
}
// static
bool WindowCycleController::CanCycle() {
return !Shell::Get()->session_controller()->IsScreenLocked() &&
!Shell::IsSystemModalWindowOpen() &&
!Shell::Get()->screen_pinning_controller()->IsPinned() &&
!window_util::IsAnyWindowDragged() &&
!Shell::Get()->desks_controller()->AreDesksBeingModified();
}
// static
void WindowCycleController::RegisterProfilePrefs(PrefRegistrySimple* registry) {
registry->RegisterBooleanPref(prefs::kAltTabPerDesk, DesksMruType::kAllDesks);
}
void WindowCycleController::HandleCycleWindow(WindowCyclingDirection direction,
bool same_app_only) {
if (!CanCycle())
return;
const bool should_start_alt_tab = !IsCycling();
if (should_start_alt_tab)
StartCycling(same_app_only);
Step(direction, /*starting_alt_tab_or_switching_mode=*/should_start_alt_tab);
}
void WindowCycleController::HandleKeyboardNavigation(
KeyboardNavDirection direction) {
// If the UI is not shown yet, discard the event.
if (!CanCycle() || !IsCycling() || !window_cycle_list_->cycle_view() ||
!IsValidKeyboardNavigation(direction)) {
return;
}
switch (direction) {
// Pressing the Up arrow key moves the focus from the window cycle list
// to the tab slider button.
case KeyboardNavDirection::kUp:
DCHECK(!IsTabSliderFocused() && IsInteractiveAltTabModeAllowed());
window_cycle_list_->SetFocusTabSlider(true);
// Focusing the alt-tab mode button announces the current mode.
Shell::Get()
->accessibility_controller()
->TriggerAccessibilityAlertWithMessage(l10n_util::GetStringUTF8(
IsAltTabPerActiveDesk()
? IDS_ASH_ALT_TAB_CURRENT_DESK_MODE_SELECTED_TITLE
: IDS_ASH_ALT_TAB_ALL_DESKS_MODE_SELECTED_TITLE));
break;
// Pressing the Down arrow key does the opposite of the Up arrow key.
case KeyboardNavDirection::kDown: {
DCHECK(IsTabSliderFocused());
window_cycle_list_->SetFocusTabSlider(false);
aura::Window* target_window = window_cycle_list_->GetTargetWindow();
// Cannot press the Down arrow key if there is no window.
DCHECK(target_window);
// Announce the selected window in the window cycle list.
Shell::Get()
->accessibility_controller()
->TriggerAccessibilityAlertWithMessage(
l10n_util::GetStringFUTF8(IDS_ASH_ALT_TAB_WINDOW_SELECTED_TITLE,
target_window->GetTitle()));
break;
}
// Pressing the Left or Right arrow keys cycles through the window list
// or switches alt-tab mode depending on which component is focused.
case KeyboardNavDirection::kRight:
case KeyboardNavDirection::kLeft:
if (!IsTabSliderFocused()) {
// Cycling through the window list if focusing the window.
HandleCycleWindow(direction == KeyboardNavDirection::kRight
? WindowCyclingDirection::kForward
: WindowCyclingDirection::kBackward);
} else {
// Switch the mode if focusing the button. Navigating right triggers
// the right button corresponding to the active desk mode. On the other
// hand, navigating left enables the all-desk mode.
OnModeChanged(direction == KeyboardNavDirection::kRight,
ModeSwitchSource::kKeyboard);
}
break;
case KeyboardNavDirection::kInvalid:
default:
NOTREACHED();
}
}
void WindowCycleController::Drag(float delta_x) {
DCHECK(window_cycle_list_);
window_cycle_list_->Drag(delta_x);
}
void WindowCycleController::StartFling(float velocity_x) {
DCHECK(window_cycle_list_);
window_cycle_list_->StartFling(velocity_x);
}
void WindowCycleController::StartCycling(bool same_app_only) {
Shell* shell = Shell::Get();
// Close the wallpaper preview if it is open to prevent visual glitches where
// the window view item for the preview is transparent
// (http://crbug.com/895265).
shell->wallpaper_controller()->MaybeClosePreviewWallpaper();
shell->event_rewriter_controller()->SetAltDownRemappingEnabled(false);
// End overview as the window cycle list takes over window switching.
shell->overview_controller()->EndOverview(
OverviewEndAction::kStartedWindowCycle);
// Close all desk bars as the window cycle list takes over window switching.
if (auto* desk_bar_controller =
shell->desks_controller()->desk_bar_controller()) {
desk_bar_controller->CloseAllDeskBars();
}
WindowCycleController::WindowList window_list = CreateWindowList();
SaveCurrentActiveDeskAndWindow(window_list);
window_cycle_list_ =
std::make_unique<WindowCycleList>(window_list, same_app_only);
event_filter_ = std::make_unique<WindowCycleEventFilter>();
base::UmaHistogramBoolean(kSameAppWindowCycleIsSameAppHistogramName,
same_app_only);
if (!same_app_only) {
base::RecordAction(base::UserMetricsAction("WindowCycleController_Cycle"));
base::UmaHistogramCounts100(kAltTabItemsHistogramName, window_list.size());
if (IsInteractiveAltTabModeAllowed()) {
// When alt-tab interactive mode is available, report the initial alt-tab
// mode which indicates the user's preferred mode.
base::UmaHistogramEnumeration(kAltTabInitialModeHistogramName,
IsAltTabPerActiveDesk()
? AltTabMode::kCurrentDesk
: AltTabMode::kAllDesks);
}
}
desks_observation_.Observe(DesksController::Get());
}
void WindowCycleController::CompleteCycling() {
DCHECK(window_cycle_list_);
window_cycle_list_->set_user_did_accept(true);
StopCycling();
}
void WindowCycleController::CancelCycling() {
StopCycling();
}
void WindowCycleController::MaybeResetCycleList() {
if (!IsCycling())
return;
WindowCycleController::WindowList window_list = CreateWindowList();
SaveCurrentActiveDeskAndWindow(window_list);
DCHECK(window_cycle_list_);
window_cycle_list_->ReplaceWindows(window_list);
}
void WindowCycleController::SetFocusedWindow(aura::Window* window) {
if (!IsCycling())
return;
DCHECK(window_cycle_list_);
window_cycle_list_->SetFocusedWindow(window);
}
bool WindowCycleController::IsEventInCycleView(
const ui::LocatedEvent* event) const {
return window_cycle_list_ && window_cycle_list_->IsEventInCycleView(event);
}
aura::Window* WindowCycleController::GetWindowAtPoint(
const ui::LocatedEvent* event) {
return window_cycle_list_ ? window_cycle_list_->GetWindowAtPoint(event)
: nullptr;
}
bool WindowCycleController::IsEventInTabSliderContainer(
const ui::LocatedEvent* event) const {
return window_cycle_list_ &&
window_cycle_list_->IsEventInTabSliderContainer(event);
}
bool WindowCycleController::IsWindowListVisible() const {
return window_cycle_list_ && window_cycle_list_->ShouldShowUi();
}
bool WindowCycleController::IsInteractiveAltTabModeAllowed() const {
return Shell::Get()->desks_controller()->GetNumberOfDesks() > 1;
}
bool WindowCycleController::IsAltTabPerActiveDesk() const {
return IsInteractiveAltTabModeAllowed() && active_user_pref_service_ &&
active_user_pref_service_->GetBoolean(prefs::kAltTabPerDesk);
}
bool WindowCycleController::IsSwitchingMode() const {
return IsInteractiveAltTabModeAllowed() && is_switching_mode_;
}
bool WindowCycleController::IsTabSliderFocused() const {
return IsInteractiveAltTabModeAllowed() &&
window_cycle_list_->IsTabSliderFocused();
}
void WindowCycleController::OnModeChanged(bool per_desk,
ModeSwitchSource source) {
DCHECK(IsInteractiveAltTabModeAllowed() && IsCycling());
// Save to the active user prefs.
auto* prefs = Shell::Get()->session_controller()->GetActivePrefService();
if (!prefs) {
// Can be null in tests.
return;
}
// Avoid an unnecessary update if any.
if (per_desk == prefs->GetBoolean(prefs::kAltTabPerDesk))
return;
prefs->SetBoolean(prefs::kAltTabPerDesk, per_desk);
// Report the alt-tab mode the user switches to and the source of switch.
if (!window_cycle_list_->same_app_only()) {
base::UmaHistogramEnumeration(
kAltTabSwitchModeHistogramName,
per_desk ? AltTabMode::kCurrentDesk : AltTabMode::kAllDesks);
base::UmaHistogramEnumeration(kAltTabModeSwitchSourceHistogramName, source);
}
// Announce the new mode and the updated window selection via ChromeVox.
aura::Window* target_window = window_cycle_list_->GetTargetWindow();
const std::string mode_switched_string = l10n_util::GetStringUTF8(
per_desk ? IDS_ASH_ALT_TAB_CURRENT_DESK_MODE_SELECTED_TITLE
: IDS_ASH_ALT_TAB_ALL_DESKS_MODE_SELECTED_TITLE);
// A ChromeVox string announcing the selected window in the window cycle list
// or no recent items if there's no window in the list.
const std::string window_selected_string =
target_window
? l10n_util::GetStringFUTF8(IDS_ASH_ALT_TAB_WINDOW_SELECTED_TITLE,
target_window->GetTitle())
: l10n_util::GetStringUTF8(IDS_ASH_OVERVIEW_NO_RECENT_ITEMS);
switch (source) {
case ModeSwitchSource::kClick:
Shell::Get()
->accessibility_controller()
->TriggerAccessibilityAlertWithMessage(base::JoinString(
{mode_switched_string, window_selected_string}, " "));
// If the user clicks the mode button, remove the focus from it.
window_cycle_list_->SetFocusTabSlider(false);
break;
case ModeSwitchSource::kKeyboard:
// Additionally, during keyboard navigation, notify that the user can
// press the Down arrow key to navigate among the cycle windows if the
// list is not empty.
Shell::Get()
->accessibility_controller()
->TriggerAccessibilityAlertWithMessage(base::JoinString(
{mode_switched_string, window_selected_string,
target_window ? l10n_util::GetStringUTF8(
IDS_ASH_ALT_TAB_FOCUS_WINDOW_LIST_TITLE)
: std::string()},
" "));
break;
default:
NOTREACHED();
}
}
void WindowCycleController::OnActiveUserPrefServiceChanged(
PrefService* pref_service) {
active_user_pref_service_ = pref_service;
InitFromUserPrefs();
}
void WindowCycleController::OnDeskAdded(const Desk* desk, bool from_undo) {
CancelCycling();
}
void WindowCycleController::OnDeskRemoved(const Desk* desk) {
CancelCycling();
}
//////////////////////////////////////////////////////////////////////////////
// WindowCycleController, private:
WindowCycleController::WindowList WindowCycleController::CreateWindowList() {
WindowList window_list = BuildWindowListForWindowCycling(
IsAltTabPerActiveDesk() ? kActiveDesk : kAllDesks);
// Window cycle list windows will handle showing their transient related
// windows, so if a window in |window_list| has a transient root also in
// |window_list|, we can remove it as the transient root will handle showing
// the window.
window_util::EnsureTransientRoots(&window_list);
return window_list;
}
MruWindowTracker::WindowList
WindowCycleController::BuildWindowListForWindowCycling(
DesksMruType desks_mru_type) {
const auto window_list =
Shell::Get()->mru_window_tracker()->BuildWindowForCycleWithPipList(
desks_mru_type);
SnapGroupController* snap_group_controller = SnapGroupController::Get();
if (!snap_group_controller) {
return window_list;
}
MruWindowTracker::WindowList adjusted_window_list;
for (aura::Window* window : window_list) {
// The latter-activated window in a snap group should have been added. Skip
// inserting to avoid duplicates.
if (base::Contains(adjusted_window_list, window)) {
continue;
}
if (SnapGroup* snap_group =
snap_group_controller->GetSnapGroupForGivenWindow(window)) {
// Insert the windows if they belong to a group following the order of the
// actual window layout, i.e. primary snapped window comes first followed
// by the secondary snapped window.
adjusted_window_list.push_back(
snap_group->GetPhysicallyLeftOrTopWindow());
adjusted_window_list.push_back(
snap_group->GetPhysicallyRightOrBottomWindow());
} else {
adjusted_window_list.push_back(window);
}
}
return adjusted_window_list;
}
void WindowCycleController::SaveCurrentActiveDeskAndWindow(
const WindowCycleController::WindowList& window_list) {
active_desk_container_id_before_cycle_ =
desks_util::GetActiveDeskContainerId();
active_window_before_window_cycle_ = GetActiveWindow(window_list);
}
void WindowCycleController::Step(WindowCyclingDirection direction,
bool starting_alt_tab_or_switching_mode) {
DCHECK(window_cycle_list_);
window_cycle_list_->Step(direction, starting_alt_tab_or_switching_mode);
}
void WindowCycleController::StopCycling() {
// There's an edge case where `StopCycling()` is already triggered via an alt
// release event, but user doesn't release the tap on the
// `window_cycle_list_`. If we reset `window_cycle_list_` first,
// `WindowEventDispatcher::DispatchSyntheticTouchEvent` will be triggered
// because of the availability changed for the `window_cycle_list_`. Thus
// `event_filter_` will still receive the event and try to handle the event
// even though it's in the process of stopping cycling. To avoid this, we
// should remove our event filter first. Please check
// https://crbug.com/1228381 for more details.
event_filter_.reset();
desks_observation_.Reset();
const bool was_same_app_only = window_cycle_list_->same_app_only();
window_cycle_list_.reset();
// We can't use the MRU window list here to get the active window, since
// cycling can activate a window on a different desk, leading to a desk-switch
// animation launching. Getting the MRU window list for the active desk now
// will always be for the current active desk, not the target active desk.
aura::Window* active_window_after_window_cycle =
window_util::GetActiveWindow();
if (was_same_app_only) {
base::UmaHistogramEnumeration(kSameAppWindowCycleDeskModeHistogramName,
IsAltTabPerActiveDesk()
? AltTabMode::kCurrentDesk
: AltTabMode::kAllDesks);
} else if (active_window_after_window_cycle != nullptr &&
active_window_before_window_cycle_ !=
active_window_after_window_cycle) {
Shell::Get()->metrics()->task_switch_metrics_recorder().OnTaskSwitch(
TaskSwitchSource::WINDOW_CYCLE_CONTROLLER);
ReportPossibleDesksSwitchStats(active_desk_container_id_before_cycle_);
}
active_window_before_window_cycle_ = nullptr;
active_desk_container_id_before_cycle_ = kShellWindowId_Invalid;
Shell::Get()->event_rewriter_controller()->SetAltDownRemappingEnabled(true);
}
void WindowCycleController::InitFromUserPrefs() {
DCHECK(active_user_pref_service_);
pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
pref_change_registrar_->Init(active_user_pref_service_);
pref_change_registrar_->Add(
prefs::kAltTabPerDesk,
base::BindRepeating(&WindowCycleController::OnAltTabModePrefChanged,
base::Unretained(this)));
OnAltTabModePrefChanged();
}
void WindowCycleController::OnAltTabModePrefChanged() {
// Only update UI for alt-tab mode if the user is using alt-tab with the
// interactive alt-tab mode supported.
if (!IsInteractiveAltTabModeAllowed() || !IsCycling())
return;
is_switching_mode_ = true;
// Update the window cycle list.
MaybeResetCycleList();
// After the cycle is reset, imitate the same forward cycling behavior as
// starting alt-tab with `Step()`, which makes sure the correct window is
// selected and focused.
Step(WindowCyclingDirection::kForward,
/*starting_alt_tab_or_switching_mode=*/true);
// Update tab slider button UI.
window_cycle_list_->OnModePrefsChanged();
is_switching_mode_ = false;
}
bool WindowCycleController::IsValidKeyboardNavigation(
KeyboardNavDirection direction) const {
// Only allow Left and Right arrow keys if interactive alt-tab mode is not
// in use.
if (!IsInteractiveAltTabModeAllowed()) {
return direction == KeyboardNavDirection::kLeft ||
direction == KeyboardNavDirection::kRight;
}
// If the focus is on the window cycle list, the user can navigate up to
// focus the mode buttons, or left and right to change the window selection.
if (!IsTabSliderFocused())
return direction != KeyboardNavDirection::kDown;
// If the focus is on the tab slider button, the user can navigate down to
// focus the non-empty list, determined by non-null target window. The user
// can only navigate left while focusing the right button and vice versa.
const bool per_desk = IsAltTabPerActiveDesk();
return (direction == KeyboardNavDirection::kDown &&
window_cycle_list_->GetTargetWindow()) ||
(per_desk && direction == KeyboardNavDirection::kLeft) ||
(!per_desk && direction == KeyboardNavDirection::kRight);
}
} // namespace ash