chromium/ash/system/accessibility/autoclick_scroll_bubble_controller.cc

// Copyright 2019 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/system/accessibility/autoclick_scroll_bubble_controller.h"

#include "ash/bubble/bubble_constants.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/tray/tray_background_view.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/wm/collision_detection/collision_detection_utils.h"
#include "ash/wm/work_area_insets.h"
#include "ash/wm/workspace/workspace_layout_manager.h"
#include "ash/wm/workspace_controller.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/layer.h"
#include "ui/display/manager/display_manager.h"
#include "ui/events/event_utils.h"
#include "ui/views/border.h"

namespace ash {

namespace {
// Autoclick scroll menu constants.
constexpr int kAutoclickScrollMenuSizeDips = 192;
const int kScrollPointBufferDips = 16;
const int kScrollRectBufferDips = 8;

struct Position {
  int distance;
  views::BubbleBorder::Arrow arrow;
  bool is_horizontal;
};

bool comparePositions(Position first, Position second) {
  return first.distance < second.distance;
}
}  // namespace

AutoclickScrollBubbleController::AutoclickScrollBubbleController() = default;

AutoclickScrollBubbleController::~AutoclickScrollBubbleController() {
  if (bubble_widget_ && !bubble_widget_->IsClosed())
    bubble_widget_->CloseNow();

  bubble_view_ = nullptr;
  scroll_view_ = nullptr;
  bubble_widget_ = nullptr;
}

void AutoclickScrollBubbleController::UpdateAnchorRect(
    gfx::Rect rect,
    views::BubbleBorder::Arrow alignment) {
  menu_bubble_rect_ = rect;
  menu_bubble_alignment_ = alignment;
  if (set_scroll_rect_ || !bubble_view_)
    return;
  bubble_view_->UpdateAnchorRect(rect, alignment);
}

void AutoclickScrollBubbleController::SetScrollPosition(
    gfx::Rect scroll_bounds_in_dips,
    const gfx::Point& scroll_point_in_dips) {
  // TODO(katie): Support multiple displays.

  if (!bubble_view_)
    return;

  // Adjust the insets to be the same on all sides, so that when the bubble
  // lays out it isn't too close on the top or bottom.
  bubble_view_->UpdateInsets(
      gfx::Insets::VH(kBubbleMenuPadding, kBubbleMenuPadding));

  aura::Window* window = Shell::GetPrimaryRootWindow();
  gfx::Rect work_area =
      WorkAreaInsets::ForWindow(window)->user_work_area_bounds();

  // If the on-screen width and height of the scroll bounds are larger than a
  // threshold size, simply offset the scroll menu bubble from the scroll point
  // position. This ensures the scroll bubble does not end up too far away from
  // the user's scroll point.
  gfx::Rect on_screen_scroll_bounds(scroll_bounds_in_dips);
  on_screen_scroll_bounds.Intersect(work_area);
  if (on_screen_scroll_bounds.width() > 2 * kAutoclickScrollMenuSizeDips &&
      on_screen_scroll_bounds.height() > 2 * kAutoclickScrollMenuSizeDips) {
    set_scroll_rect_ = true;
    gfx::Rect anchor =
        gfx::Rect(scroll_point_in_dips.x(), scroll_point_in_dips.y(), 0, 0);
    // Buffer around the point so that the scroll bubble does not overlap it.
    // ScrollBubbleController will automatically layout to avoid edges.
    // Aims to lay out like the context menu, at 16x16 to the bottom right.
    anchor.Inset(gfx::Insets::VH(0, -kScrollPointBufferDips));
    bubble_view_->UpdateAnchorRect(anchor,
                                   views::BubbleBorder::Arrow::LEFT_TOP);
    return;
  }

  // Otherwise, the scrollable area bounds are small compared to the scroll
  // scroll bubble, so we should not overlap the scroll bubble if possible.
  // Try to show the scroll bubble on the side of the rect closest to the point.
  // Determine whether there will be space to show the scroll bubble between the
  // scroll bounds and display bounds.
  work_area.Inset(kAutoclickScrollMenuSizeDips);
  scroll_bounds_in_dips.Inset(-kScrollRectBufferDips);
  bool fits_left = scroll_bounds_in_dips.x() > work_area.x();
  bool fits_right = scroll_bounds_in_dips.right() < work_area.right();
  bool fits_above = scroll_bounds_in_dips.y() > work_area.y();
  bool fits_below = scroll_bounds_in_dips.bottom() < work_area.bottom();

  // The scroll bubble will fit outside the bounds of the scrollable area.
  // Determine its exact position based on the scroll point:
  // First, try to lay out the scroll bubble on the side of the scroll bounds
  // closest to the scroll point.
  //
  //   ------------------
  //   |                |
  //   |               *|  <-- Here, the scroll point is closest to the right
  //   |                |      edge so the bubble should be laid out on the
  //   |                |      right of the scroll bounds.
  //   ------------------
  //
  std::vector<Position> positions;
  if (fits_right) {
    positions.push_back(
        {scroll_bounds_in_dips.right() - scroll_point_in_dips.x(),
         // Put it on the right. Note that in RTL languages right and left
         // arrows
         // are swapped.
         base::i18n::IsRTL() ? views::BubbleBorder::Arrow::RIGHT_CENTER
                             : views::BubbleBorder::Arrow::LEFT_CENTER,
         true});
  }
  if (fits_left) {
    positions.push_back({scroll_point_in_dips.x() - scroll_bounds_in_dips.x(),
                         // Put it on the left. Note that in RTL languages right
                         // and left arrows are swapped.
                         base::i18n::IsRTL()
                             ? views::BubbleBorder::Arrow::LEFT_CENTER
                             : views::BubbleBorder::Arrow::RIGHT_CENTER,
                         true});
  }
  if (fits_below) {
    positions.push_back(
        {scroll_bounds_in_dips.bottom() - scroll_point_in_dips.y(),
         views::BubbleBorder::Arrow::TOP_CENTER, false});
  }
  if (fits_above) {
    positions.push_back({scroll_point_in_dips.y() - scroll_bounds_in_dips.y(),
                         views::BubbleBorder::Arrow::BOTTOM_CENTER, false});
  }

  // If the scroll bubble doesn't fit between the scroll bounds and display
  // edges, re-pin the scroll bubble to the menu bubble.
  // This may not ever happen except on low-resolution screens.
  set_scroll_rect_ = !positions.empty();
  if (!set_scroll_rect_) {
    bubble_view_->UpdateInsets(gfx::Insets::TLBR(
        0, kBubbleMenuPadding, kBubbleMenuPadding, kBubbleMenuPadding));
    UpdateAnchorRect(menu_bubble_rect_, menu_bubble_alignment_);
    return;
  }

  // Sort positions based on distance.
  std::stable_sort(positions.begin(), positions.end(), comparePositions);

  // Position on the optimal side depending on the shortest distance.
  if (positions.front().is_horizontal) {
    // Center it vertically on the scroll point.
    bubble_view_->UpdateAnchorRect(
        gfx::Rect(scroll_bounds_in_dips.x(), scroll_point_in_dips.y(),
                  scroll_bounds_in_dips.width(), 0),
        positions.at(0).arrow);
  } else {
    // Center horizontally on scroll point.
    bubble_view_->UpdateAnchorRect(
        gfx::Rect(scroll_point_in_dips.x(), scroll_bounds_in_dips.y(), 0,
                  scroll_bounds_in_dips.height()),
        positions.at(0).arrow);
  }
}

void AutoclickScrollBubbleController::ShowBubble(
    gfx::Rect anchor_rect,
    views::BubbleBorder::Arrow alignment) {
  // Ignore if bubble widget already exists.
  if (bubble_widget_)
    return;

  DCHECK(!bubble_view_);

  TrayBubbleView::InitParams init_params;
  init_params.delegate = GetWeakPtr();
  // Anchor within the overlay container.
  init_params.parent_window =
      Shell::GetContainer(Shell::GetPrimaryRootWindow(),
                          kShellWindowId_AccessibilityBubbleContainer);
  init_params.anchor_mode = TrayBubbleView::AnchorMode::kRect;
  init_params.anchor_rect = anchor_rect;
  // The widget's shadow is drawn below and on the sides of the scroll view.
  // Do not inset the top, so that when the scroll bubble is shown below the
  // menu bubble it lays out directly below the menu bubble's shadow, at a
  // height of kBubbleMenuPadding.
  init_params.insets = gfx::Insets::TLBR(
      0, kBubbleMenuPadding, kBubbleMenuPadding, kBubbleMenuPadding);
  init_params.preferred_width = kAutoclickScrollMenuSizeDips;
  init_params.max_height = kAutoclickScrollMenuSizeDips;
  init_params.translucent = true;
  init_params.type = TrayBubbleView::TrayBubbleType::kAccessibilityBubble;

  bubble_view_ = new AutoclickScrollBubbleView(init_params);
  bubble_view_->SetArrow(alignment);

  scroll_view_ = new AutoclickScrollView();
  scroll_view_->SetBorder(views::CreateEmptyBorder(
      gfx::Insets::TLBR(kUnifiedTopShortcutSpacing, 0, 0, 0)));
  bubble_view_->AddChildView(scroll_view_.get());

  bubble_widget_ = views::BubbleDialogDelegateView::CreateBubble(bubble_view_);
  TrayBackgroundView::InitializeBubbleAnimations(bubble_widget_);
  CollisionDetectionUtils::MarkWindowPriorityForCollisionDetection(
      bubble_widget_->GetNativeWindow(),
      CollisionDetectionUtils::RelativePriority::kAutomaticClicksScrollMenu);
  bubble_view_->InitializeAndShowBubble();
}

void AutoclickScrollBubbleController::CloseBubble() {
  if (!bubble_widget_ || bubble_widget_->IsClosed())
    return;
  bubble_widget_->Close();
}

void AutoclickScrollBubbleController::SetBubbleVisibility(bool is_visible) {
  if (!bubble_widget_)
    return;

  if (is_visible)
    bubble_widget_->Show();
  else
    bubble_widget_->Hide();
}

void AutoclickScrollBubbleController::ClickOnBubble(gfx::Point location_in_dips,
                                                    int mouse_event_flags) {
  if (!bubble_widget_ || !bubble_view_)
    return;

  // Change the event location bounds to be relative to the menu bubble.
  location_in_dips -= bubble_view_->GetBoundsInScreen().OffsetFromOrigin();

  // Generate synthesized mouse events for the click.
  const ui::MouseEvent press_event(
      ui::EventType::kMousePressed, location_in_dips, location_in_dips,
      ui::EventTimeForNow(), mouse_event_flags | ui::EF_LEFT_MOUSE_BUTTON,
      ui::EF_LEFT_MOUSE_BUTTON);
  const ui::MouseEvent release_event(
      ui::EventType::kMouseReleased, location_in_dips, location_in_dips,
      ui::EventTimeForNow(), mouse_event_flags | ui::EF_LEFT_MOUSE_BUTTON,
      ui::EF_LEFT_MOUSE_BUTTON);

  // Send the press/release events to the widget's root view for processing.
  bubble_widget_->GetRootView()->OnMousePressed(press_event);
  bubble_widget_->GetRootView()->OnMouseReleased(release_event);
}

bool AutoclickScrollBubbleController::ContainsPointInScreen(
    const gfx::Point& point) {
  return bubble_view_ && bubble_view_->GetBoundsInScreen().Contains(point);
}

void AutoclickScrollBubbleController::BubbleViewDestroyed() {
  bubble_view_ = nullptr;
  bubble_widget_ = nullptr;
  scroll_view_ = nullptr;
}

std::u16string AutoclickScrollBubbleController::GetAccessibleNameForBubble() {
  return l10n_util::GetStringUTF16(IDS_ASH_AUTOCLICK_SCROLL_BUBBLE);
}

void AutoclickScrollBubbleController::HideBubble(
    const TrayBubbleView* bubble_view) {
  // This function is currently not unused for bubbles of type
  // `kAccessibilityBubble`, so can leave this empty.
}

}  // namespace ash