| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // 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/public/cpp/shell_window_ids.h" |
| #include "ash/shelf/shelf.h" |
| #include "ash/shell.h" |
| #include "ash/system/tray/tray_background_view.h" |
| #include "ash/system/tray/tray_constants.h" |
| #include "ash/system/unified/unified_system_tray_view.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/display/manager/display_manager.h" |
| #include "ui/events/event_utils.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() {} |
| |
| AutoclickScrollBubbleController::~AutoclickScrollBubbleController() { |
| if (bubble_widget_ && !bubble_widget_->IsClosed()) |
| bubble_widget_->CloseNow(); |
| } |
| |
| void AutoclickScrollBubbleController::UpdateAnchorRect( |
| gfx::Rect rect, |
| views::BubbleBorder::Arrow alignment) { |
| menu_bubble_rect_ = rect; |
| menu_bubble_alignment_ = alignment; |
| if (set_scroll_rect_) |
| 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. |
| |
| // 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(kUnifiedMenuPadding, kUnifiedMenuPadding)); |
| |
| 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(-kScrollPointBufferDips, 0); |
| 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, kAutoclickScrollMenuSizeDips); |
| scroll_bounds_in_dips.Inset(-kScrollRectBufferDips, -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( |
| 0, kUnifiedMenuPadding, kUnifiedMenuPadding, kUnifiedMenuPadding)); |
| 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 = this; |
| // Anchor within the overlay container. |
| init_params.parent_window = Shell::GetContainer( |
| Shell::GetPrimaryRootWindow(), kShellWindowId_AutoclickContainer); |
| 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 kUnifiedMenuPadding. |
| init_params.insets = gfx::Insets(0, kUnifiedMenuPadding, kUnifiedMenuPadding, |
| kUnifiedMenuPadding); |
| init_params.min_width = kAutoclickScrollMenuSizeDips; |
| init_params.max_width = kAutoclickScrollMenuSizeDips; |
| init_params.max_height = kAutoclickScrollMenuSizeDips; |
| init_params.corner_radius = kUnifiedTrayCornerRadius; |
| init_params.has_shadow = false; |
| init_params.translucent = true; |
| bubble_view_ = new AutoclickScrollBubbleView(init_params); |
| bubble_view_->SetArrow(alignment); |
| |
| scroll_view_ = new AutoclickScrollView(); |
| scroll_view_->SetBorder( |
| views::CreateEmptyBorder(kUnifiedTopShortcutSpacing, 0, 0, 0)); |
| bubble_view_->AddChildView(scroll_view_); |
| scroll_view_->SetPaintToLayer(); |
| scroll_view_->layer()->SetFillsBoundsOpaquely(false); |
| |
| 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_) |
| 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::ET_MOUSE_PRESSED, 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::ET_MOUSE_RELEASED, 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; |
| } |
| |
| } // namespace ash |