chromium/ash/wm/tablet_mode/tablet_mode_multitask_menu_controller.cc

// Copyright 2022 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/tablet_mode/tablet_mode_multitask_menu_controller.h"

#include "ash/accelerators/debug_commands.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/shell_delegate.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_multitask_cue_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_multitask_menu.h"
#include "ash/wm/tablet_mode/tablet_mode_window_manager.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "base/functional/bind.h"
#include "ui/events/event.h"
#include "ui/events/event_target.h"
#include "ui/events/types/event_type.h"
#include "ui/wm/core/coordinate_conversion.h"

namespace ash {

namespace {

// The dimensions of the region that can activate the multitask menu.
constexpr gfx::SizeF kHitRegionSize(200.f, 100.f);

// Returns true if `window` can show the menu and `screen_location` is in the
// menu hit bounds.
bool HitTestRect(aura::Window* window, const gfx::PointF& screen_location) {
  if (!TabletModeMultitaskMenuController::CanShowMenu(window)) {
    return false;
  }
  const gfx::RectF window_bounds(window->GetBoundsInScreen());
  gfx::RectF hit_region(window_bounds);
  hit_region.ClampToCenteredSize(kHitRegionSize);
  hit_region.set_y(window_bounds.y());
  return hit_region.Contains(screen_location);
}

// Returns the toplevel window for `target`, or active window if there isn't
// one. Note this can be the multitask menu, since we drag up in the menu
// coordinates.
aura::Window* GetTargetWindow(aura::Window* target) {
  views::Widget* widget = views::Widget::GetTopLevelWidgetForNativeView(target);
  return widget ? widget->GetNativeWindow() : window_util::GetActiveWindow();
}

}  // namespace

TabletModeMultitaskMenuController::TabletModeMultitaskMenuController()
    : multitask_cue_controller_(
          std::make_unique<TabletModeMultitaskCueController>()) {
  Shell::Get()->AddPreTargetHandler(this);
}

TabletModeMultitaskMenuController::~TabletModeMultitaskMenuController() {
  // The cue needs to be destroyed first so that it doesn't do any work when
  // window activation changes as a result of destroying `this`.
  multitask_cue_controller_.reset();

  Shell::Get()->RemovePreTargetHandler(this);
}

// static
bool TabletModeMultitaskMenuController::CanShowMenu(aura::Window* window) {
  // Cannot show the menu in the lock screen, or in app/kiosk mode.
  if (Shell::Get()->session_controller()->IsScreenLocked() ||
      Shell::Get()->session_controller()->IsRunningInAppMode()) {
    return false;
  }

  auto* window_state = WindowState::Get(window);
  return window_state && window_state->CanMaximize() &&
         window_state->CanResize() && !window_state->IsFloated() &&
         !window_state->IsPinned();
}

void TabletModeMultitaskMenuController::ShowMultitaskMenu(
    aura::Window* window) {
  MaybeCreateMultitaskMenu(window);
  if (multitask_menu_) {
    multitask_menu_->Animate(/*show=*/true);
  }
}

void TabletModeMultitaskMenuController::ResetMultitaskMenu() {
  multitask_menu_.reset();
}

void TabletModeMultitaskMenuController::OnTouchEvent(ui::TouchEvent* event) {
  if (is_drag_active_) {
    if (!reserved_for_gesture_sent_) {
      reserved_for_gesture_sent_ = true;
      event->SetFlags(event->flags() | ui::EF_RESERVED_FOR_GESTURE);
      return;
    }
    event->StopPropagation();
    event->ForceProcessGesture();
  }
}

void TabletModeMultitaskMenuController::OnGestureEvent(
    ui::GestureEvent* event) {
  aura::Window* target = static_cast<aura::Window*>(event->target());
  aura::Window* window = GetTargetWindow(target);
  if (!window ||
      !Shell::Get()->shell_delegate()->AllowDefaultTouchActions(window)) {
    return;
  }

  const ui::GestureEventDetails details = event->details();
  // Do not handle PEN and ERASER events. PEN events can come from stylus
  // device.
  if (details.primary_pointer_type() == ui::EventPointerType::kPen ||
      details.primary_pointer_type() == ui::EventPointerType::kEraser) {
    return;
  }

  gfx::PointF screen_location = event->location_f();
  wm::ConvertPointToScreen(target, &screen_location);

  // Save the window coordinates to pass to the menu.
  gfx::PointF window_location = event->location_f();
  aura::Window::ConvertPointToTarget(target, window, &window_location);
  switch (event->type()) {
    case ui::EventType::kGestureScrollBegin:
      if (std::fabs(details.scroll_y_hint()) <
          std::fabs(details.scroll_x_hint())) {
        return;
      }
      is_drag_active_ = false;
      reserved_for_gesture_sent_ = false;
      if (details.scroll_y_hint() > 0 && HitTestRect(window, screen_location)) {
        // We may need to recreate `multitask_menu_` on the new target window.
        target_window_for_test_ = window;
        multitask_menu_ =
            std::make_unique<TabletModeMultitaskMenu>(this, window);
        multitask_cue_controller_->OnMenuOpened(window);
        multitask_menu_->BeginDrag(window_location.y(), /*down=*/true);
        event->SetHandled();
        is_drag_active_ = true;
      } else if (details.scroll_y_hint() < 0 && multitask_menu_ &&
                 gfx::RectF(
                     multitask_menu_->widget()->GetWindowBoundsInScreen())
                     .Contains(screen_location)) {
        // If the menu is open and scroll up begins, only handle events inside
        // the menu to avoid consuming scroll events outside the menu.
        // TODO(b/279816982): Fix the cue reappearing when the menu is dismissed
        // by swiping up or not dragging far enough.
        multitask_menu_->BeginDrag(window_location.y(), /*down=*/false);
        event->SetHandled();
        is_drag_active_ = true;
      }
      break;
    case ui::EventType::kGestureScrollUpdate:
      if (is_drag_active_ && multitask_menu_) {
        multitask_menu_->UpdateDrag(window_location.y(),
                                    /*down=*/details.scroll_y() > 0);
        event->SetHandled();
      }
      break;
    case ui::EventType::kGestureScrollEnd:
    case ui::EventType::kGestureEnd:
      // If an unsupported gesture is sent, make sure we reset `is_drag_active_`
      // to stop consuming events.
      if (is_drag_active_ && multitask_menu_) {
        multitask_menu_->EndDrag();
        event->SetHandled();
      }
      is_drag_active_ = false;
      reserved_for_gesture_sent_ = false;
      break;
    case ui::EventType::kScrollFlingStart:
      if (!is_drag_active_) {
        return;
      }
      // Normally EventType::kGestureScrollBegin will fire first and have
      // already created the multitask menu, however occasionally
      // EventType::kScrollFlingStart may fire first (https://crbug.com/821237).
      target_window_for_test_ = window;
      MaybeCreateMultitaskMenu(window);
      if (multitask_menu_) {
        multitask_menu_->Animate(details.velocity_y() > 0);
        event->SetHandled();
      }
      break;
    default:
      if (is_drag_active_ && multitask_menu_) {
        // Do not reset `is_drag_active_` to handle until the gesture ends.
        event->SetHandled();
      }
      break;
  }
}

void TabletModeMultitaskMenuController::MaybeCreateMultitaskMenu(
    aura::Window* window) {
  if (!multitask_menu_ && CanShowMenu(window)) {
    multitask_menu_ = std::make_unique<TabletModeMultitaskMenu>(this, window);
    multitask_cue_controller_->OnMenuOpened(window);
  }
}

}  // namespace ash