chromium/ash/system/accessibility/floating_accessibility_controller.cc

// Copyright 2020 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/floating_accessibility_controller.h"

#include "ash/accessibility/accessibility_controller.h"
#include "ash/bubble/bubble_constants.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/accessibility/floating_menu_utils.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 "base/check.h"
#include "base/notreached.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/views/border.h"

namespace ash {

namespace {

constexpr int kFloatingMenuHeight = 64;
constexpr base::TimeDelta kAnimationDuration = base::Milliseconds(150);

// Implements scoped (RAII) activation for a FloatingAccessibilityBubbleView
// object.
class ScopedBubbleViewActivator {
 public:
  explicit ScopedBubbleViewActivator(
      ash::FloatingAccessibilityBubbleView* bubble_view)
      : bubble_view_(bubble_view) {
    DCHECK(bubble_view_);
    bubble_view_->SetCanActivate(true);
  }

  ScopedBubbleViewActivator(const ScopedBubbleViewActivator&) = delete;
  ScopedBubbleViewActivator& operator=(const ScopedBubbleViewActivator&) =
      delete;

  ~ScopedBubbleViewActivator() { bubble_view_->SetCanActivate(false); }

 private:
  raw_ptr<ash::FloatingAccessibilityBubbleView> bubble_view_ = nullptr;
};

}  // namespace

FloatingAccessibilityController::FloatingAccessibilityController(
    AccessibilityController* accessibility_controller)
    : accessibility_controller_(accessibility_controller) {
  Shell::Get()->locale_update_controller()->AddObserver(this);
  accessibility_controller_->AddObserver(this);
}
FloatingAccessibilityController::~FloatingAccessibilityController() {
  Shell::Get()->locale_update_controller()->RemoveObserver(this);
  accessibility_controller_->RemoveObserver(this);
  if (bubble_widget_ && !bubble_widget_->IsClosed()) {
    bubble_widget_->CloseNow();
  }
}

void FloatingAccessibilityController::Show(FloatingMenuPosition position) {
  // Kiosk check.
  if (!Shell::Get()->session_controller()->IsRunningInAppMode()) {
    NOTREACHED()
        << "Floating accessibility menu can only be run in a kiosk session.";
  }

  DCHECK(!bubble_view_);

  TrayBubbleView::InitParams init_params;
  init_params.delegate = GetWeakPtr();
  // Our view uses SettingsBubbleContainer since it is activatable and is
  // included in the collision detection logic.
  init_params.parent_window = Shell::GetContainer(
      Shell::GetPrimaryRootWindow(), kShellWindowId_SettingBubbleContainer);
  init_params.anchor_mode = TrayBubbleView::AnchorMode::kRect;
  // The widget's shadow is drawn below and on the sides of the view, with a
  // width of kCollisionWindowWorkAreaInsetsDp. Set the top inset to 0 to ensure
  // the detailed view is drawn at kCollisionWindowWorkAreaInsetsDp above the
  // bubble menu when the position is at the bottom of the screen. The space
  // between the bubbles belongs to the detailed view bubble's shadow.
  init_params.insets = gfx::Insets::TLBR(0, kCollisionWindowWorkAreaInsetsDp,
                                         kCollisionWindowWorkAreaInsetsDp,
                                         kCollisionWindowWorkAreaInsetsDp);
  init_params.max_height = kFloatingMenuHeight;
  init_params.translucent = true;
  init_params.close_on_deactivate = false;
  init_params.type = TrayBubbleView::TrayBubbleType::kAccessibilityBubble;
  bubble_view_ = new FloatingAccessibilityBubbleView(init_params);

  menu_view_ = new FloatingAccessibilityView(this);
  menu_view_->SetBorder(views::CreateEmptyBorder(
      gfx::Insets::TLBR(kUnifiedTopShortcutSpacing, 0, 0, 0)));
  bubble_view_->AddChildView(menu_view_.get());
  bubble_view_->SetFocusBehavior(views::View::FocusBehavior::ACCESSIBLE_ONLY);

  bubble_widget_ = views::BubbleDialogDelegateView::CreateBubble(bubble_view_);
  // Keep bubble view deactivated not to steal focus from input by clicks on
  // dictation or on-screen keyboard buttons.
  bubble_view_->SetCanActivate(false);
  TrayBackgroundView::InitializeBubbleAnimations(bubble_widget_);
  bubble_view_->InitializeAndShowBubble();

  menu_view_->Initialize();

  SetMenuPosition(position);
}

void FloatingAccessibilityController::SetMenuPosition(
    FloatingMenuPosition new_position) {
  if (!menu_view_ || !bubble_view_ || !bubble_widget_) {
    return;
  }

  // Update the menu view's UX if the position has changed, or if it's not the
  // default position (because that can change with language direction).
  if (position_ != new_position ||
      new_position == FloatingMenuPosition::kSystemDefault) {
    menu_view_->SetMenuPosition(new_position);
  }
  position_ = new_position;

  // If this is the default system position, pick the position based on the
  // language direction.
  if (new_position == FloatingMenuPosition::kSystemDefault) {
    new_position = DefaultSystemFloatingMenuPosition();
  }

  gfx::Rect new_bounds = GetOnScreenBoundsForFloatingMenuPosition(
      menu_view_->GetPreferredSize(), new_position);

  gfx::Rect resting_bounds =
      CollisionDetectionUtils::AdjustToFitMovementAreaByGravity(
          display::Screen::GetScreen()->GetDisplayNearestWindow(
              bubble_widget_->GetNativeWindow()),
          new_bounds);
  // Un-inset the bounds to get the widget's bounds, which includes the drop
  // shadow.
  resting_bounds.Inset(gfx::Insets::TLBR(0, -kCollisionWindowWorkAreaInsetsDp,
                                         -kCollisionWindowWorkAreaInsetsDp,
                                         -kCollisionWindowWorkAreaInsetsDp));

  if (bubble_widget_->GetWindowBoundsInScreen() == resting_bounds) {
    return;
  }

  ui::ScopedLayerAnimationSettings settings(
      bubble_widget_->GetLayer()->GetAnimator());
  settings.SetPreemptionStrategy(
      ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
  settings.SetTransitionDuration(kAnimationDuration);
  settings.SetTweenType(gfx::Tween::EASE_OUT);
  bubble_widget_->SetBounds(resting_bounds);

  if (detailed_menu_controller_) {
    detailed_menu_controller_->UpdateAnchorRect(
        resting_bounds, GetAnchorAlignmentForFloatingMenuPosition(position_));
  }
}

void FloatingAccessibilityController::FocusOnMenu() {
  // Temporarily activate floating accessibility bubble view when processing
  // a keyboard shortcut to allow getting focus.
  ScopedBubbleViewActivator activator(bubble_view_);

  bubble_view_->GetFocusManager()->ClearFocus();
  bubble_view_->GetFocusManager()->AdvanceFocus(false /* reverse */);
}

void FloatingAccessibilityController::OnDetailedMenuEnabled(bool enabled) {
  if (enabled) {
    detailed_menu_controller_ =
        std::make_unique<FloatingAccessibilityDetailedController>(this);
    gfx::Rect anchor_rect = bubble_view_->GetBoundsInScreen();
    anchor_rect.Inset(gfx::Insets::TLBR(0, -kCollisionWindowWorkAreaInsetsDp,
                                        -kCollisionWindowWorkAreaInsetsDp,
                                        -kCollisionWindowWorkAreaInsetsDp));
    detailed_menu_controller_->Show(
        anchor_rect, GetAnchorAlignmentForFloatingMenuPosition(position_));
    menu_view_->SetDetailedViewShown(true);
  } else {
    detailed_menu_controller_.reset();
    // We may need to update the autoclick bounds.
    Shell::Get()
        ->accessibility_controller()
        ->UpdateAutoclickMenuBoundsIfNeeded();
    bubble_view_->GetFocusManager()->ClearFocus();
  }
}

void FloatingAccessibilityController::OnLayoutChanged() {
  if (on_layout_change_) {
    on_layout_change_.Run();
  }
  SetMenuPosition(position_);
}

void FloatingAccessibilityController::OnDetailedMenuClosed() {
  detailed_menu_controller_.reset();

  if (!menu_view_) {
    return;
  }
  menu_view_->SetDetailedViewShown(false);
  if (bubble_widget_->IsActive()) {
    menu_view_->FocusOnDetailedViewButton();
  }
}

views::Widget* FloatingAccessibilityController::GetBubbleWidget() {
  return bubble_widget_;
}

void FloatingAccessibilityController::BubbleViewDestroyed() {
  bubble_view_ = nullptr;
  bubble_widget_ = nullptr;
  menu_view_ = nullptr;
}

std::u16string FloatingAccessibilityController::GetAccessibleNameForBubble() {
  return l10n_util::GetStringUTF16(IDS_ASH_FLOATING_ACCESSIBILITY_MAIN_MENU);
}

void FloatingAccessibilityController::HideBubble(
    const TrayBubbleView* bubble_view) {}

void FloatingAccessibilityController::OnLocaleChanged() {
  // Layout update is needed when language changes between LTR and RTL, if the
  // position is the system default.
  if (position_ == FloatingMenuPosition::kSystemDefault) {
    SetMenuPosition(position_);
  }
}

void FloatingAccessibilityController::OnAccessibilityStatusChanged() {
  // Some features may change the available screen area(docked magnifier), we
  // will update the location of the menu in such cases.
  SetMenuPosition(position_);
  if (detailed_menu_controller_) {
    detailed_menu_controller_->OnAccessibilityStatusChanged();
  }
}

void FloatingAccessibilityController::OnDisplayMetricsChanged(
    const display::Display& display,
    uint32_t changed_metrics) {
  SetMenuPosition(position_);
}

}  // namespace ash