chromium/ash/wm/window_cycle/window_cycle_list.cc

// 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_list.h"

#include "ash/accessibility/accessibility_controller.h"
#include "ash/app_list/app_list_controller_impl.h"
#include "ash/frame_throttler/frame_throttling_controller.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/root_window_controller.h"
#include "ash/shell.h"
#include "ash/shell_delegate.h"
#include "ash/system/tray/tray_background_view.h"
#include "ash/wm/mru_window_tracker.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_controller.h"
#include "ash/wm/window_cycle/window_cycle_view.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "base/check.h"
#include "base/containers/flat_set.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/ranges/algorithm.h"
#include "base/trace_event/trace_event.h"
#include "ui/aura/scoped_window_targeter.h"
#include "ui/aura/window.h"
#include "ui/aura/window_targeter.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_type.h"
#include "ui/compositor/presentation_time_recorder.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/events/event.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/core/window_animations.h"

namespace ash {

namespace {

constexpr char kSameAppWindowCycleSkippedWindowsHistogramName[] =
    "Ash.WindowCycleController.SameApp.SkippedWindows";

constexpr char kEnterWindowCyclePresentationHistogramName[] =
    "Ash.WindowCycleController.Enter.PresentationTime";

constexpr base::TimeDelta kEnterPresentationMaxLatency = base::Seconds(2);

bool g_disable_initial_delay = false;

// Delay before the UI fade in animation starts. This is so users can switch
// quickly between windows without bringing up the UI.
constexpr base::TimeDelta kShowDelayDuration = base::Milliseconds(150);

// The alt-tab cycler widget is not activatable (except when ChromeVox is on),
// so we use WindowTargeter to send input events to the widget.
class CustomWindowTargeter : public aura::WindowTargeter {
 public:
  explicit CustomWindowTargeter(aura::Window* tab_cycler)
      : tab_cycler_(tab_cycler) {}
  CustomWindowTargeter(const CustomWindowTargeter&) = delete;
  CustomWindowTargeter& operator=(const CustomWindowTargeter&) = delete;
  ~CustomWindowTargeter() override = default;

  // aura::WindowTargeter:
  ui::EventTarget* FindTargetForEvent(ui::EventTarget* root,
                                      ui::Event* event) override {
    if (event->IsLocatedEvent())
      return aura::WindowTargeter::FindTargetForEvent(root, event);
    return tab_cycler_;
  }

 private:
  raw_ptr<aura::Window> tab_cycler_;
};

gfx::Point ConvertEventToScreen(const ui::LocatedEvent* event) {
  aura::Window* target = static_cast<aura::Window*>(event->target());
  aura::Window* event_root = target->GetRootWindow();
  gfx::Point event_screen_point = event->root_location();
  wm::ConvertPointToScreen(event_root, &event_screen_point);
  return event_screen_point;
}

bool IsWindowInSnapGroup(aura::Window* window) {
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  return snap_group_controller &&
         snap_group_controller->GetSnapGroupForGivenWindow(window);
}

// Returns the mru window with the existence of snap groups. If a snap group is
// at the beginning of the window cycle list, we need to check the activation
// order of the two windows in the snap group since the window list has been
// reordered to reflect the actual window layout with the primarily snapped
// window comes before the secondarily snapped window, which makes the front
// window in the window lists not guaranteed to be the mru window.
aura::Window* GetMruWindow(
    const std::vector<raw_ptr<aura::Window, VectorExperimental>>& windows) {
  aura::Window* front_window = windows.front();
  if (IsWindowInSnapGroup(front_window)) {
    SnapGroup* snap_group =
        SnapGroupController::Get()->GetSnapGroupForGivenWindow(front_window);
    aura::Window* window1 = snap_group->window1();
    aura::Window* window2 = snap_group->window2();
    CHECK_EQ(front_window, window1);
    if (window_util::IsStackedBelow(window1, window2)) {
      return window2;
    }
  }

  return front_window;
}

}  // namespace

WindowCycleList::WindowCycleList(const WindowList& windows, bool same_app_only)
    : windows_(windows), same_app_only_(same_app_only) {
  if (!ShouldShowUi())
    Shell::Get()->mru_window_tracker()->SetIgnoreActivations(true);

  active_window_before_window_cycle_ = window_util::GetActiveWindow();

  if (same_app_only) {
    MakeSameAppOnly();
  }

  for (aura::Window* window : windows_) {
    window->AddObserver(this);
  }

  if (ShouldShowUi()) {
    // Disable the tab scrubber so three finger scrolling doesn't scrub tabs as
    // well.
    Shell::Get()->shell_delegate()->SetTabScrubberChromeOSEnabled(false);

    if (g_disable_initial_delay) {
      InitWindowCycleView();
    } else {
      show_ui_timer_.Start(FROM_HERE, kShowDelayDuration, this,
                           &WindowCycleList::InitWindowCycleView);
    }
  }
}

WindowCycleList::~WindowCycleList() {
  if (!ShouldShowUi())
    Shell::Get()->mru_window_tracker()->SetIgnoreActivations(false);

  Shell::Get()->shell_delegate()->SetTabScrubberChromeOSEnabled(true);

  for (aura::Window* window : windows_) {
    window->RemoveObserver(this);
  }

  if (cycle_ui_widget_)
    cycle_ui_widget_->Close();

  // Store the target window before |cycle_view_| is destroyed.
  aura::Window* target_window = nullptr;

  // |this| is responsible for notifying |cycle_view_| when windows are
  // destroyed. Since |this| is going away, clobber |cycle_view_|. Otherwise
  // there will be a race where a window closes after now but before the
  // Widget::Close() call above actually destroys |cycle_view_|. See
  // crbug.com/681207
  if (cycle_view_) {
    target_window = GetTargetWindow();
    cycle_view_->DestroyContents();
  }

  // While the cycler widget is shown, the windows listed in the cycler is
  // marked as force-visible and don't contribute to occlusion. In order to
  // work occlusion calculation properly, we need to activate a window after
  // the widget has been destroyed. See b/138914552.
  if (!windows_.empty() && user_did_accept_) {
    if (!target_window)
      target_window = windows_[current_index_];
    MaybeReportNonSameAppSkippedWindows(target_window);
    SelectWindow(target_window);
  }
  Shell::Get()->frame_throttling_controller()->EndThrottling();
}

aura::Window* WindowCycleList::GetTargetWindow() {
  return cycle_view_->target_window();
}

void WindowCycleList::ReplaceWindows(const WindowList& windows) {
  RemoveAllWindows();
  windows_ = windows;

  if (same_app_only_) {
    MakeSameAppOnly();
  }

  for (aura::Window* new_window : windows_) {
    new_window->AddObserver(this);
  }

  if (cycle_view_)
    cycle_view_->UpdateWindows(windows_);
}

void WindowCycleList::Step(WindowCyclingDirection direction,
                           bool starting_alt_tab_or_switching_mode) {
  if (windows_.empty())
    return;

  last_cycling_direction_ = direction;

  // If the position of the window cycle list is out-of-sync with the currently
  // selected item, scroll to the selected item and then step.
  if (cycle_view_) {
    aura::Window* selected_window = GetTargetWindow();
    if (selected_window)
      Scroll(GetIndexOfWindow(selected_window) - current_index_);
  }

  int offset = direction == WindowCyclingDirection::kForward ? 1 : -1;
  // When the window focus should be reset and the first window in the MRU
  // cycle list is not the latest active one before entering alt-tab, focus
  // it instead of the second window. This occurs when the user is in overview
  // mode, all windows are minimized, or all windows are in other desks.
  //
  // Note:
  // Simply checking the active status of the first window won't work
  // because when the ChromeVox is enabled, the widget is activatable, so the
  // first window in MRU becomes inactive.
  if (starting_alt_tab_or_switching_mode &&
      direction == WindowCyclingDirection::kForward &&
      (active_window_before_window_cycle_ != windows_[0])) {
    offset = 0;
    current_index_ = 0;
  }

  SetFocusedWindow(windows_[GetOffsettedWindowIndex(offset)]);
  Scroll(offset);
}

void WindowCycleList::Drag(float delta_x) {
  DCHECK(cycle_view_);
  cycle_view_->Drag(delta_x);
}

void WindowCycleList::StartFling(float velocity_x) {
  DCHECK(cycle_view_);
  cycle_view_->StartFling(velocity_x);
}

void WindowCycleList::SetFocusedWindow(aura::Window* window) {
  if (windows_.empty())
    return;

  if (ShouldShowUi() && cycle_view_)
    cycle_view_->SetTargetWindow(windows_[GetIndexOfWindow(window)]);
}

void WindowCycleList::SetFocusTabSlider(bool focus) {
  DCHECK(cycle_view_);
  cycle_view_->SetFocusTabSlider(focus);
}

bool WindowCycleList::IsTabSliderFocused() const {
  DCHECK(cycle_view_);
  return cycle_view_->IsTabSliderFocused();
}

bool WindowCycleList::IsEventInCycleView(const ui::LocatedEvent* event) const {
  return cycle_view_ &&
         cycle_view_->GetBoundsInScreen().Contains(ConvertEventToScreen(event));
}

aura::Window* WindowCycleList::GetWindowAtPoint(const ui::LocatedEvent* event) {
  return cycle_view_
             ? cycle_view_->GetWindowAtPoint(ConvertEventToScreen(event))
             : nullptr;
}

bool WindowCycleList::IsEventInTabSliderContainer(
    const ui::LocatedEvent* event) const {
  return cycle_view_ &&
         cycle_view_->IsEventInTabSliderContainer(ConvertEventToScreen(event));
}

bool WindowCycleList::ShouldShowUi() const {
  // Show alt-tab when there are at least two windows to pick from alt-tab, or
  // when there is at least a window to switch to by switching to the different
  // mode.
  if (!Shell::Get()
           ->window_cycle_controller()
           ->IsInteractiveAltTabModeAllowed()) {
    return windows_.size() > 1u;
  }

  int total_window_in_all_desks = GetNumberOfWindowsAllDesks();
  return windows_.size() > 1u ||
         (windows_.size() <= 1u &&
          static_cast<size_t>(total_window_in_all_desks) > windows_.size());
}

void WindowCycleList::OnModePrefsChanged() {
  if (cycle_view_)
    cycle_view_->OnModePrefsChanged();
}

// static
void WindowCycleList::SetDisableInitialDelayForTesting(bool disabled) {
  g_disable_initial_delay = disabled;
}

void WindowCycleList::OnWindowDestroying(aura::Window* window) {
  window->RemoveObserver(this);

  WindowList::iterator i = base::ranges::find(windows_, window);
  CHECK(i != windows_.end());
  int removed_index = static_cast<int>(i - windows_.begin());
  windows_.erase(i);
  if (current_index_ > removed_index ||
      current_index_ == static_cast<int>(windows_.size())) {
    current_index_--;
  }

  // Reset |active_window_before_window_cycle_| to avoid a dangling pointer.
  if (window == active_window_before_window_cycle_)
    active_window_before_window_cycle_ = nullptr;

  if (cycle_view_) {
    auto* new_target_window =
        windows_.empty() ? nullptr : windows_[current_index_].get();
    cycle_view_->HandleWindowDestruction(window, new_target_window);

    if (windows_.empty()) {
      // This deletes us.
      Shell::Get()->window_cycle_controller()->CancelCycling();
      return;
    }
  }
}

void WindowCycleList::OnDisplayMetricsChanged(const display::Display& display,
                                              uint32_t changed_metrics) {
  if (cycle_ui_widget_ &&
      display.id() ==
          display::Screen::GetScreen()
              ->GetDisplayNearestWindow(cycle_ui_widget_->GetNativeWindow())
              .id() &&
      (changed_metrics & (DISPLAY_METRIC_BOUNDS | DISPLAY_METRIC_ROTATION))) {
    Shell::Get()->window_cycle_controller()->CancelCycling();
    // |this| is deleted.
    return;
  }
}

void WindowCycleList::RemoveAllWindows() {
  for (aura::Window* window : windows_) {
    window->RemoveObserver(this);

    if (cycle_view_)
      cycle_view_->HandleWindowDestruction(window, nullptr);
  }

  windows_.clear();
  current_index_ = 0;
  window_selected_ = false;
}

void WindowCycleList::InitWindowCycleView() {
  if (cycle_view_)
    return;

  TRACE_EVENT0("ui", "WindowCycleList::InitWindowCycleView");

  aura::Window* root_window = Shell::GetRootWindowForNewWindows();

  auto presentation_time_recorder = CreatePresentationTimeHistogramRecorder(
      root_window->layer()->GetCompositor(),
      kEnterWindowCyclePresentationHistogramName, "",
      kEnterPresentationMaxLatency);
  presentation_time_recorder->RequestNext();

  // Close any tray bubbles that are opened before creating the cycle view.
  StatusAreaWidget* status_area_widget =
      RootWindowController::ForWindow(root_window)->GetStatusAreaWidget();
  for (TrayBackgroundView* tray_button : status_area_widget->tray_buttons()) {
    if (tray_button->is_active())
      tray_button->CloseBubble();
  }

  cycle_view_ = new WindowCycleView(root_window, windows_, same_app_only_);
  const bool is_interactive_alt_tab_mode_allowed =
      Shell::Get()->window_cycle_controller()->IsInteractiveAltTabModeAllowed();
  DCHECK(!windows_.empty() || is_interactive_alt_tab_mode_allowed);

  // Only set target window and scroll to the window when alt-tab is not empty.
  if (!windows_.empty()) {
    DCHECK(static_cast<int>(windows_.size()) > current_index_);
    cycle_view_->SetTargetWindow(windows_[current_index_]);
    cycle_view_->ScrollToWindow(windows_[current_index_]);
  }

  // We need to activate the widget if ChromeVox is enabled as ChromeVox
  // relies on activation.
  const bool spoken_feedback_enabled =
      Shell::Get()->accessibility_controller()->spoken_feedback().enabled();

  views::Widget* widget = new views::Widget();
  views::Widget::InitParams params(
      views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
      views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
  params.delegate = cycle_view_.get();
  params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
  params.layer_type = ui::LAYER_NOT_DRAWN;

  // Don't let the alt-tab cycler be activatable. This lets the currently
  // activated window continue to be in the foreground. This may affect
  // things such as video automatically pausing/playing.
  if (!spoken_feedback_enabled)
    params.activatable = views::Widget::InitParams::Activatable::kNo;
  params.accept_events = true;
  params.name = "WindowCycleList (Alt+Tab)";
  // TODO(estade): make sure nothing untoward happens when the lock screen
  // or a system modal dialog is shown.
  params.parent = root_window->GetChildById(kShellWindowId_OverlayContainer);
  params.bounds = cycle_view_->GetTargetBounds();

  widget->Init(std::move(params));
  widget->Show();
  cycle_view_->FadeInLayer();
  cycle_ui_widget_ = widget;

  // Since this window is not activated, grab events.
  if (!spoken_feedback_enabled) {
    window_targeter_ = std::make_unique<aura::ScopedWindowTargeter>(
        widget->GetNativeWindow()->GetRootWindow(),
        std::make_unique<CustomWindowTargeter>(widget->GetNativeWindow()));
  }
  // Close the app list, if it's open in clamshell mode.
  if (!display::Screen::GetScreen()->InTabletMode()) {
    Shell::Get()->app_list_controller()->DismissAppList();
  }

  Shell::Get()->frame_throttling_controller()->StartThrottling(windows_);
}

void WindowCycleList::SelectWindow(aura::Window* window) {
  // If the list has only one window, the window can be selected twice (in
  // Scroll() and the destructor). This causes ARC PIP windows to be restored
  // twice, which leads to a wrong window state.
  if (window_selected_)
    return;

  if (window->GetProperty(kPipOriginalWindowKey)) {
    window_util::ExpandArcPipWindow();
  } else {
    window->Show();
    WindowState::Get(window)->Activate();
  }

  window_selected_ = true;
}

void WindowCycleList::Scroll(int offset) {
  if (windows_.size() == 1)
    SelectWindow(windows_[0]);

  if (!ShouldShowUi()) {
    // When there is only one window, we should give feedback to the user. If
    // the window is minimized, we should also show it.
    if (windows_.size() == 1)
      wm::AnimateWindow(windows_[0], wm::WINDOW_ANIMATION_TYPE_BOUNCE);
    return;
  }

  DCHECK(static_cast<size_t>(current_index_) < windows_.size());
  current_index_ = GetOffsettedWindowIndex(offset);

  if (current_index_ > 1)
    InitWindowCycleView();

  // The windows should not shift position when selecting when there's enough
  // room to display all windows.
  if (cycle_view_ && cycle_view_->CalculatePreferredSize({}).width() ==
                         cycle_view_->CalculateMaxWidth()) {
    cycle_view_->ScrollToWindow(windows_[current_index_]);
  }
}

void WindowCycleList::MakeSameAppOnly() {
  CHECK(same_app_only_);
  if (windows_.size() < 2) {
    return;
  }

  const std::string* const mru_window_app_id =
      GetMruWindow(windows_)->GetProperty(kAppIDKey);
  if (!mru_window_app_id) {
    return;
  }
  windows_.erase(
      base::ranges::remove_if(windows_.begin(), windows_.end(),
                              [&mru_window_app_id](aura::Window* window) {
                                const auto* const app_id =
                                    window->GetProperty(kAppIDKey);
                                return !app_id || *app_id != *mru_window_app_id;
                              }),
      windows_.end());
}

int WindowCycleList::GetOffsettedWindowIndex(int offset) const {
  DCHECK(!windows_.empty());

  const int offsetted_index =
      (current_index_ + offset + windows_.size()) % windows_.size();
  DCHECK(windows_[offsetted_index]);

  return offsetted_index;
}

int WindowCycleList::GetIndexOfWindow(aura::Window* window) const {
  auto target_window = base::ranges::find(windows_, window);
  DCHECK(target_window != windows_.end());
  return std::distance(windows_.begin(), target_window);
}

int WindowCycleList::GetNumberOfWindowsAllDesks() const {
  WindowCycleController* window_cycle_controller =
      Shell::Get()->window_cycle_controller();

  // If alt-tab mode is not available, the alt-tab defaults to all-desks mode
  // and can obtain the number of all windows easily from `windows_.size()`.
  CHECK(window_cycle_controller->IsInteractiveAltTabModeAllowed());
  return window_cycle_controller->BuildWindowListForWindowCycling(kAllDesks)
      .size();
}

void WindowCycleList::MaybeReportNonSameAppSkippedWindows(
    aura::Window* target_window) const {
  if (!same_app_only_ || windows_.size() < 2 || current_index_ == 0) {
    return;
  }

  WindowCycleController* window_cycle_controller =
      Shell::Get()->window_cycle_controller();
  const bool per_active_desk = window_cycle_controller->IsAltTabPerActiveDesk()
                                   ? kActiveDesk
                                   : kAllDesks;
  const WindowList original_windows =
      window_cycle_controller->BuildWindowListForWindowCycling(
          per_active_desk ? kActiveDesk : kAllDesks);

  const std::string* const mru_window_app_id =
      target_window->GetProperty(kAppIDKey);
  if (!mru_window_app_id) {
    return;
  }

  // The window at index 0 is the window cycling started on. It can't be a
  // skipped window, so start at index 1.
  int start = 1;
  int increment = 1;

  // If we're cycling backwards, start from the end and work backwards.
  if (last_cycling_direction_ == WindowCyclingDirection::kBackward) {
    start = original_windows.size() - 1;
    increment = -1;
  }

  // Count up the skipped windows between the starting window and the chosen
  // window.
  int skipped_windows = 0;
  aura::Window* current_window = nullptr;
  for (int i = start; i >= 0 && i < static_cast<int>(original_windows.size()) &&
                      current_window != target_window;
       i += increment) {
    current_window = original_windows[i];
    const auto* const app_id = current_window->GetProperty(kAppIDKey);
    if (!app_id || *app_id != *mru_window_app_id) {
      skipped_windows++;
    }
  }
  // Make sure looping stopped because we found the window.
  DCHECK_EQ(current_window, target_window);

  base::UmaHistogramCounts100(kSameAppWindowCycleSkippedWindowsHistogramName,
                              skipped_windows);
}

}  // namespace ash