chromium/ash/wm/window_cycle/window_cycle_event_filter.cc

// Copyright 2016 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_event_filter.h"

#include "ash/accelerators/debug_commands.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/display/screen_ash.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/wm/window_cycle/window_cycle_list.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "base/functional/bind.h"
#include "components/prefs/pref_service.h"
#include "ui/events/event.h"
#include "ui/events/types/event_type.h"
#include "ui/wm/core/coordinate_conversion.h"

namespace ash {
namespace {

// The distance a user has to move their mouse from |initial_mouse_location_|
// before this stops filtering mouse events.
constexpr int kMouseMovementThreshold = 5;

// Is reverse scrolling for mouse wheel on.
bool IsReverseScrollOn() {
  PrefService* pref =
      Shell::Get()->session_controller()->GetActivePrefService();
  return pref->GetBoolean(prefs::kMouseReverseScroll);
}

// Returns whether `event` is a trigger key (tab, left, right, w (when
// debugging)).
bool IsTriggerKey(ui::KeyEvent* event) {
  const ui::KeyboardCode key_code = event->key_code();
  const bool interactive_trigger_key =
      (key_code == ui::VKEY_LEFT || key_code == ui::VKEY_RIGHT);

  const bool nav_trigger_key =
      Shell::Get()
          ->window_cycle_controller()
          ->IsInteractiveAltTabModeAllowed() &&
      (key_code == ui::VKEY_UP || key_code == ui::VKEY_DOWN ||
       key_code == ui::VKEY_LEFT || key_code == ui::VKEY_RIGHT);

  return key_code == ui::VKEY_TAB ||
         (debug::DeveloperAcceleratorsEnabled() && key_code == ui::VKEY_W) ||
         interactive_trigger_key || nav_trigger_key;
}

// Returns whether `event` is an exit key (return, space).
bool IsExitKey(ui::KeyEvent* event) {
  return event->key_code() == ui::VKEY_RETURN ||
         event->key_code() == ui::VKEY_SPACE;
}

}  // namespace

WindowCycleEventFilter::WindowCycleEventFilter()
    : initial_mouse_location_(
          display::Screen::GetScreen()->GetCursorScreenPoint()) {
  Shell::Get()->AddPreTargetHandler(this);
  // Handling release of "Alt" must come before other pretarget handlers
  // (specifically, the partial screenshot handler). See crbug.com/651939
  // We can't do all key event handling that early though because it prevents
  // other accelerators (like triggering a partial screenshot) from working.
  Shell::Get()->AddPreTargetHandler(&alt_release_handler_,
                                    ui::EventTarget::Priority::kSystem);
}

WindowCycleEventFilter::~WindowCycleEventFilter() {
  Shell::Get()->RemovePreTargetHandler(this);
  Shell::Get()->RemovePreTargetHandler(&alt_release_handler_);
}

void WindowCycleEventFilter::OnKeyEvent(ui::KeyEvent* event) {
  // Until the alt key is released, all key events except the trigger key press
  // (which is handled by the accelerator controller to call Step) are handled
  // by this window cycle controller: https://crbug.com/340339. When the window
  // cycle list exists, right + left arrow keys are considered trigger keys and
  // those two are handled by this.
  const bool is_trigger_key = IsTriggerKey(event);
  const bool is_exit_key = IsExitKey(event);

  if (!is_trigger_key || event->type() != ui::EventType::kKeyPressed) {
    event->StopPropagation();
  }

  if (is_trigger_key)
    HandleTriggerKey(event);
  else if (is_exit_key)
    Shell::Get()->window_cycle_controller()->CompleteCycling();
  else if (event->key_code() == ui::VKEY_ESCAPE)
    Shell::Get()->window_cycle_controller()->CancelCycling();
}

void WindowCycleEventFilter::OnMouseEvent(ui::MouseEvent* event) {
  if (!has_user_used_mouse_)
    SetHasUserUsedMouse(event);

  if (has_user_used_mouse_) {
    WindowCycleController* window_cycle_controller =
        Shell::Get()->window_cycle_controller();
    const bool cycle_list_is_visible =
        window_cycle_controller->IsWindowListVisible();
    if (cycle_list_is_visible)
      ProcessMouseEvent(event);

    if (window_cycle_controller->IsEventInCycleView(event) ||
        !cycle_list_is_visible) {
      return;
    }
  }

  // Prevent mouse clicks from doing anything while the Alt+Tab UI is active
  // <crbug.com/641171> but don't interfere with drag and drop operations
  // <crbug.com/660945>.
  if (event->type() != ui::EventType::kMouseDragged &&
      event->type() != ui::EventType::kMouseReleased) {
    event->StopPropagation();
  }
}

void WindowCycleEventFilter::OnScrollEvent(ui::ScrollEvent* event) {
  // EventType::kScrollFlingCancel means a touchpad swipe has started.
  if (event->type() == ui::EventType::kScrollFlingCancel) {
    scroll_data_ = ScrollData();
    return;
  }

  // EventType::kScrollFlingStart means a touchpad swipe has ended.
  if (event->type() == ui::EventType::kScrollFlingStart) {
    scroll_data_.reset();
    return;
  }

  DCHECK_EQ(ui::EventType::kScroll, event->type());

  if (ProcessEventImpl(event->finger_count(), event->x_offset(),
                       event->y_offset())) {
    event->SetHandled();
    event->StopPropagation();
  }
}

void WindowCycleEventFilter::OnGestureEvent(ui::GestureEvent* event) {
  if (Shell::Get()->window_cycle_controller()->IsEventInTabSliderContainer(
          event)) {
    // Return immediately if the event is on the tab slider container. Pass
    // the event to the tab slider buttons to handle it.
    return;
  }
  ProcessGestureEvent(event);
}

void WindowCycleEventFilter::HandleTriggerKey(ui::KeyEvent* event) {
  const ui::KeyboardCode key_code = event->key_code();
  if (event->type() == ui::EventType::kKeyReleased) {
    repeat_timer_.Stop();
  } else if (ShouldRepeatKey(event)) {
    repeat_timer_.Start(
        FROM_HERE, base::Milliseconds(180),
        base::BindRepeating(
            &WindowCycleController::HandleCycleWindow,
            base::Unretained(Shell::Get()->window_cycle_controller()),
            GetWindowCyclingDirection(event), /*same_app_only=*/false));
  } else if (key_code == ui::VKEY_UP || key_code == ui::VKEY_DOWN ||
             key_code == ui::VKEY_LEFT || key_code == ui::VKEY_RIGHT) {
    Shell::Get()->window_cycle_controller()->HandleKeyboardNavigation(
        GetKeyboardNavDirection(event));
  }
}

bool WindowCycleEventFilter::ShouldRepeatKey(ui::KeyEvent* event) const {
  return event->type() == ui::EventType::kKeyPressed && event->is_repeat() &&
         !repeat_timer_.IsRunning();
}

void WindowCycleEventFilter::SetHasUserUsedMouse(ui::MouseEvent* event) {
  if (event->type() != ui::EventType::kMouseMoved &&
      event->type() != ui::EventType::kMouseEntered &&
      event->type() != ui::EventType::kMouseExited) {
    // If a user clicks/drags/scrolls mouse wheel, then they have used the
    // mouse.
    has_user_used_mouse_ = true;
    return;
  }

  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);
  if ((initial_mouse_location_ - event_screen_point).Length() >
      kMouseMovementThreshold) {
    has_user_used_mouse_ = true;
  }
}

void WindowCycleEventFilter::ProcessMouseEvent(ui::MouseEvent* event) {
  auto* window_cycle_controller = Shell::Get()->window_cycle_controller();
  if (event->type() == ui::EventType::kMousePressed &&
      !window_cycle_controller->IsEventInCycleView(event)) {
    // Close the window cycle list if a user clicks outside of it.
    window_cycle_controller->CancelCycling();
    return;
  }

  if (event->IsMouseWheelEvent()) {
    if (!scroll_data_)
      scroll_data_ = ScrollData();
    const ui::MouseWheelEvent* wheel_event = event->AsMouseWheelEvent();
    const float y_offset = wheel_event->y_offset();
    // Convert mouse wheel events into three-finger scrolls for window cycle
    // list and also swap y offset with x offset.
    if (ProcessEventImpl(/*finger_count=*/3,
                         IsReverseScrollOn() ? y_offset : -y_offset,
                         wheel_event->x_offset())) {
      event->SetHandled();
      event->StopPropagation();
    }
  }
}

void WindowCycleEventFilter::ProcessGestureEvent(ui::GestureEvent* event) {
  bool should_complete_cycling = false;
  switch (event->type()) {
    case ui::EventType::kGestureTap:
    case ui::EventType::kGestureTapDown:
    case ui::EventType::kGestureDoubleTap:
    case ui::EventType::kGestureTapUnconfirmed:
    case ui::EventType::kGestureTwoFingerTap:
    case ui::EventType::kGestureLongPress:
    case ui::EventType::kGestureLongTap: {
      tapped_window_ =
          Shell::Get()->window_cycle_controller()->GetWindowAtPoint(
              event->AsLocatedEvent());
      break;
    }
    case ui::EventType::kGestureTapCancel:
      // Do nothing because the event after this one determines whether we
      // scrolled or tapped.
      break;
    case ui::EventType::kGestureScrollBegin: {
      tapped_window_ = nullptr;
      if (!Shell::Get()->window_cycle_controller()->IsEventInCycleView(event))
        return;

      touch_scrolling_ = true;
      break;
    }
    case ui::EventType::kGestureScrollUpdate: {
      if (!touch_scrolling_)
        return;

      Shell::Get()->window_cycle_controller()->Drag(
          event->details().scroll_x());
      break;
    }
    case ui::EventType::kScrollFlingStart: {
      tapped_window_ = nullptr;
      auto* window_cycle_controller = Shell::Get()->window_cycle_controller();
      if (!window_cycle_controller->IsEventInCycleView(event))
        return;

      // Only start a fling if the x-velocity is non-zero to avoid crashing when
      // creating a fling curve. See crbug.com/1224969.
      float velocity_x = event->details().velocity_x();
      if (velocity_x != 0.f)
        window_cycle_controller->StartFling(velocity_x);
      break;
    }
    case ui::EventType::kGestureEnd: {
      if (tapped_window_) {
        // Defer calling WindowCycleController::CompleteCycling() until we've
        // set |event| to handled and stop its propagation.
        should_complete_cycling = true;
      }
      tapped_window_ = nullptr;
      touch_scrolling_ = false;
      break;
    }
    default:
      if (tapped_window_) {
        Shell::Get()->window_cycle_controller()->SetFocusedWindow(
            tapped_window_);
        break;
      }
      return;
  }

  event->SetHandled();
  event->StopPropagation();

  if (should_complete_cycling)
    Shell::Get()->window_cycle_controller()->CompleteCycling();
}

bool WindowCycleEventFilter::ProcessEventImpl(int finger_count,
                                              float delta_x,
                                              float delta_y) {
  if (!scroll_data_)
    return false;

  if (finger_count != 2 && finger_count != 3) {
    scroll_data_.reset();
    return false;
  }

  if (scroll_data_->finger_count != 0 &&
      scroll_data_->finger_count != finger_count) {
    scroll_data_.reset();
    return false;
  }

  if (finger_count == 2 && !window_util::IsNaturalScrollOn()) {
    // Two finger swipe from left to right should move the list right regardless
    // of natural scroll settings.
    delta_x = -delta_x;
  }

  scroll_data_->scroll_x += delta_x;
  scroll_data_->scroll_y += delta_y;

  const bool moved = CycleWindowCycleList(finger_count, scroll_data_->scroll_x,
                                          scroll_data_->scroll_y);

  if (moved)
    scroll_data_ = ScrollData();
  scroll_data_->finger_count = finger_count;
  return moved;
}

bool WindowCycleEventFilter::CycleWindowCycleList(int finger_count,
                                                  float scroll_x,
                                                  float scroll_y) {
  if (finger_count != 2 && finger_count != 3)
    return false;

  auto* window_cycle_controller = Shell::Get()->window_cycle_controller();
  if (!window_cycle_controller->IsCycling() ||
      std::fabs(scroll_x) < std::fabs(scroll_y) ||
      std::fabs(scroll_x) < kHorizontalThresholdDp) {
    return false;
  }

  window_cycle_controller->HandleCycleWindow(
      scroll_x > 0 ? WindowCycleController::WindowCyclingDirection::kForward
                   : WindowCycleController::WindowCyclingDirection::kBackward);
  return true;
}

WindowCycleController::WindowCyclingDirection
WindowCycleEventFilter::GetWindowCyclingDirection(ui::KeyEvent* event) const {
  DCHECK(IsTriggerKey(event));

  // Move backward if left arrow, forward if right arrow, tab, or W. Shift flips
  // the direction.
  const bool left = event->key_code() == ui::VKEY_LEFT;
  const bool shift = event->IsShiftDown();

  return (left ^ shift)
             ? WindowCycleController::WindowCyclingDirection::kBackward
             : WindowCycleController::WindowCyclingDirection::kForward;
}

WindowCycleController::KeyboardNavDirection
WindowCycleEventFilter::GetKeyboardNavDirection(ui::KeyEvent* event) const {
  DCHECK(IsTriggerKey(event));
  switch (event->key_code()) {
    case ui::VKEY_UP:
      return WindowCycleController::KeyboardNavDirection::kUp;
    case ui::VKEY_DOWN:
      return WindowCycleController::KeyboardNavDirection::kDown;
    case ui::VKEY_LEFT:
      return WindowCycleController::KeyboardNavDirection::kLeft;
    case ui::VKEY_RIGHT:
      return WindowCycleController::KeyboardNavDirection::kRight;
    default:
      NOTREACHED();
  }
}

WindowCycleEventFilter::AltReleaseHandler::AltReleaseHandler() = default;

WindowCycleEventFilter::AltReleaseHandler::~AltReleaseHandler() = default;

void WindowCycleEventFilter::AltReleaseHandler::OnKeyEvent(
    ui::KeyEvent* event) {
  // Views uses VKEY_MENU for both left and right Alt keys.
  if (event->key_code() == ui::VKEY_MENU &&
      event->type() == ui::EventType::kKeyReleased) {
    event->StopPropagation();
    Shell::Get()->window_cycle_controller()->CompleteCycling();
    // Warning: |this| will be deleted from here on.
  }
}

}  // namespace ash