chromium/ash/system/accessibility/floating_accessibility_view.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_view.h"

#include "ash/accessibility/accessibility_controller.h"
#include "ash/ime/ime_controller_impl.h"
#include "ash/keyboard/ui/keyboard_ui_controller.h"
#include "ash/public/cpp/keyboard/keyboard_controller.h"
#include "ash/public/cpp/system_tray.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/root_window_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_provider.h"
#include "ash/system/accessibility/dictation_button_tray.h"
#include "ash/system/accessibility/floating_menu_button.h"
#include "ash/system/accessibility/select_to_speak/select_to_speak_tray.h"
#include "ash/system/ime_menu/ime_menu_tray.h"
#include "ash/system/tray/system_tray_notifier.h"
#include "ash/system/tray/tray_bubble_view.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/virtual_keyboard/virtual_keyboard_tray.h"
#include "base/functional/bind.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/color/color_id.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/border.h"
#include "ui/views/controls/separator.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/view.h"

namespace ash {

namespace {

// These constants are defined in DIP.
constexpr int kPanelPositionButtonPadding = 14;
constexpr int kPanelPositionButtonSize = 36;
constexpr int kSeparatorHeight = 16;

// The view that hides itself if all of its children are not visible.
class DynamicRowView : public views::View {
 public:
  DynamicRowView() = default;

 protected:
  // views::View:
  void ChildVisibilityChanged(views::View* child) override {
    bool any_visible = false;
    for (views::View* view : children()) {
      any_visible |= view->GetVisible();
    }
    SetVisible(any_visible);
  }
};

std::unique_ptr<views::Separator> CreateSeparator() {
  auto separator = std::make_unique<views::Separator>();
  separator->SetColorId(ui::kColorAshSystemUIMenuSeparator);
  separator->SetPreferredLength(kSeparatorHeight);
  int total_height = kUnifiedTopShortcutSpacing * 2 + kTrayItemSize;
  int separator_spacing = (total_height - kSeparatorHeight) / 2;
  separator->SetBorder(views::CreateEmptyBorder(
      gfx::Insets::TLBR(separator_spacing - kUnifiedTopShortcutSpacing, 0,
                        separator_spacing, 0)));
  return separator;
}

std::unique_ptr<views::View> CreateButtonRowContainer(int padding) {
  auto button_container = std::make_unique<DynamicRowView>();
  button_container->SetLayoutManager(std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kHorizontal,
      gfx::Insets::TLBR(0, padding, padding, padding), padding));
  return button_container;
}

std::string GetDescriptionForMovedToPosition(FloatingMenuPosition position) {
  switch (position) {
    case FloatingMenuPosition::kBottomRight:
      return l10n_util::GetStringUTF8(
          IDS_ASH_FLOATING_ACCESSIBILITY_MAIN_MENU_MOVED_BOTTOM_RIGHT);
    case FloatingMenuPosition::kBottomLeft:
      return l10n_util::GetStringUTF8(
          IDS_ASH_FLOATING_ACCESSIBILITY_MAIN_MENU_MOVED_BOTTOM_LEFT);
    case FloatingMenuPosition::kTopLeft:
      return l10n_util::GetStringUTF8(
          IDS_ASH_FLOATING_ACCESSIBILITY_MAIN_MENU_MOVED_TOP_LEFT);
    case FloatingMenuPosition::kTopRight:
      return l10n_util::GetStringUTF8(
          IDS_ASH_FLOATING_ACCESSIBILITY_MAIN_MENU_MOVED_TOP_RIGHT);
    case FloatingMenuPosition::kSystemDefault:
      NOTREACHED();
  }
}

bool IsKioskImeButtonEnabled() {
  return Shell::Get()->session_controller()->IsRunningInAppMode() &&
         base::FeatureList::IsEnabled(features::kKioskEnableImeButton) &&
         Shell::Get()->ime_controller()->GetVisibleImes().size() > 1;
}

}  // namespace

FloatingAccessibilityBubbleView::FloatingAccessibilityBubbleView(
    const TrayBubbleView::InitParams& init_params)
    : TrayBubbleView(init_params) {
  // Intercept ESC keypresses.
  AddAccelerator(ui::Accelerator(ui::VKEY_ESCAPE, ui::EF_NONE));

  GetViewAccessibility().SetRole(ax::mojom::Role::kWindow);
}

FloatingAccessibilityBubbleView::~FloatingAccessibilityBubbleView() = default;

bool FloatingAccessibilityBubbleView::IsAnchoredToStatusArea() const {
  return false;
}

bool FloatingAccessibilityBubbleView::AcceleratorPressed(
    const ui::Accelerator& accelerator) {
  DCHECK_EQ(accelerator.key_code(), ui::VKEY_ESCAPE);
  GetWidget()->Deactivate();
  return true;
}

void FloatingAccessibilityBubbleView::GetAccessibleNodeData(
    ui::AXNodeData* node_data) {
  node_data->SetNameExplicitlyEmpty();
  TrayBubbleView::GetAccessibleNodeData(node_data);
}

BEGIN_METADATA(FloatingAccessibilityBubbleView)
END_METADATA

FloatingAccessibilityView::FloatingAccessibilityView(Delegate* delegate)
    : delegate_(delegate) {
  Shelf* shelf = RootWindowController::ForTargetRootWindow()->shelf();
  std::unique_ptr<views::View> feature_buttons_container =
      CreateButtonRowContainer(kPanelPositionButtonPadding);
  dictation_button_observation_.Observe(feature_buttons_container->AddChildView(
      std::make_unique<DictationButtonTray>(
          shelf, TrayBackgroundViewCatalogName::kDictationAccesibilityWindow)));
  select_to_speak_button_observation_.Observe(
      feature_buttons_container->AddChildView(std::make_unique<
                                              SelectToSpeakTray>(
          shelf,
          TrayBackgroundViewCatalogName::kSelectToSpeakAccessibilityWindow)));
  virtual_keyboard_button_observation_.Observe(
      feature_buttons_container->AddChildView(std::make_unique<
                                              VirtualKeyboardTray>(
          shelf,
          TrayBackgroundViewCatalogName::kVirtualKeyboardAccessibilityWindow)));

  // It will be visible again as soon as any of the children becomes visible.
  feature_buttons_container->SetVisible(false);

  std::unique_ptr<views::View> tray_button_container =
      CreateButtonRowContainer(kUnifiedTopShortcutSpacing);
  a11y_tray_button_ =
      tray_button_container->AddChildView(std::make_unique<FloatingMenuButton>(
          base::BindRepeating(
              &FloatingAccessibilityView::OnA11yTrayButtonPressed,
              base::Unretained(this)),
          kUnifiedMenuAccessibilityIcon,
          IDS_ASH_FLOATING_ACCESSIBILITY_DETAILED_MENU_OPEN,
          /*flip_for_rtl*/ true));

  std::unique_ptr<views::View> position_button_container =
      CreateButtonRowContainer(kPanelPositionButtonPadding);
  position_button_ = position_button_container->AddChildView(
      std::make_unique<FloatingMenuButton>(
          base::BindRepeating(
              &FloatingAccessibilityView::OnPositionButtonPressed,
              base::Unretained(this)),
          kAutoclickPositionBottomLeftIcon,
          IDS_ASH_AUTOCLICK_OPTION_CHANGE_POSITION, /*flip_for_rtl*/ false,
          kPanelPositionButtonSize, false, /* is_a11y_togglable */ false));

  if (IsKioskImeButtonEnabled()) {
    Shell::Get()->ime_controller()->SetExtraInputOptionsEnabledState(
        /*is_extra_input_options_enabled*/ false, /*is_emoji_enabled*/ false,
        /*is_handwriting_enabled*/ false, /*is_voice_enabled*/ false);
    std::unique_ptr<views::View> ime_button_container =
        CreateButtonRowContainer(kPanelPositionButtonPadding);
    ime_button_observation_.Observe(ime_button_container->AddChildView(
        std::make_unique<ImeMenuTray>(shelf)));
    ime_button_container->SetVisible(true);
    ime_button()->SetVisiblePreferred(true);

    AddChildView(std::move(ime_button_container));
    AddChildView(CreateSeparator());
  }

  AddChildView(std::move(feature_buttons_container));
  AddChildView(std::move(tray_button_container));
  AddChildView(CreateSeparator());
  AddChildView(std::move(position_button_container));

  // Set view IDs for testing.
  position_button_->SetID(static_cast<int>(ButtonId::kPosition));
  a11y_tray_button_->SetID(static_cast<int>(ButtonId::kSettingsList));
  dictation_button()->SetID(static_cast<int>(ButtonId::kDictation));
  select_to_speak_button()->SetID(static_cast<int>(ButtonId::kSelectToSpeak));
  virtual_keyboard_button()->SetID(
      static_cast<int>(ButtonId::kVirtualKeyboard));
  if (IsKioskImeButtonEnabled()) {
    ime_button()->SetID(static_cast<int>(ButtonId::kIme));
  }
}

FloatingAccessibilityView::~FloatingAccessibilityView() {
  KeyboardController::Get()->RemoveObserver(this);
  Shell::Get()->system_tray_notifier()->RemoveSystemTrayObserver(this);
}

void FloatingAccessibilityView::Initialize() {
  Shell::Get()->system_tray_notifier()->AddSystemTrayObserver(this);
  KeyboardController::Get()->AddObserver(this);
  for (TrayBackgroundView* feature_view : {
           dictation_button(),
           select_to_speak_button(),
           virtual_keyboard_button(),
       }) {
    feature_view->Initialize();
    feature_view->CalculateTargetBounds();
    feature_view->UpdateLayout();
  }
  if (IsKioskImeButtonEnabled()) {
    ime_button()->Initialize();
    ime_button()->CalculateTargetBounds();
    ime_button()->UpdateLayout();
    ime_button()->SetVisible(true);
  }
}

void FloatingAccessibilityView::SetMenuPosition(FloatingMenuPosition position) {
  switch (position) {
    case FloatingMenuPosition::kBottomRight:
      position_button_->SetVectorIcon(kAutoclickPositionBottomRightIcon);
      return;
    case FloatingMenuPosition::kBottomLeft:
      position_button_->SetVectorIcon(kAutoclickPositionBottomLeftIcon);
      return;
    case FloatingMenuPosition::kTopLeft:
      position_button_->SetVectorIcon(kAutoclickPositionTopLeftIcon);
      return;
    case FloatingMenuPosition::kTopRight:
      position_button_->SetVectorIcon(kAutoclickPositionTopRightIcon);
      return;
    case FloatingMenuPosition::kSystemDefault:
      position_button_->SetVectorIcon(base::i18n::IsRTL()
                                          ? kAutoclickPositionBottomLeftIcon
                                          : kAutoclickPositionBottomRightIcon);
      return;
  }
}

void FloatingAccessibilityView::SetDetailedViewShown(bool shown) {
  a11y_tray_button_->SetToggled(shown);
}

void FloatingAccessibilityView::FocusOnDetailedViewButton() {
  a11y_tray_button_->RequestFocus();
}

void FloatingAccessibilityView::OnA11yTrayButtonPressed() {
  delegate_->OnDetailedMenuEnabled(!a11y_tray_button_->GetToggled());
}

void FloatingAccessibilityView::OnPositionButtonPressed() {
  FloatingMenuPosition new_position;
  // Rotate clockwise throughout the screen positions.
  switch (Shell::Get()->accessibility_controller()->GetFloatingMenuPosition()) {
    case FloatingMenuPosition::kBottomRight:
      new_position = FloatingMenuPosition::kBottomLeft;
      break;
    case FloatingMenuPosition::kBottomLeft:
      new_position = FloatingMenuPosition::kTopLeft;
      break;
    case FloatingMenuPosition::kTopLeft:
      new_position = FloatingMenuPosition::kTopRight;
      break;
    case FloatingMenuPosition::kTopRight:
      new_position = FloatingMenuPosition::kBottomRight;
      break;
    case FloatingMenuPosition::kSystemDefault:
      new_position = base::i18n::IsRTL() ? FloatingMenuPosition::kTopLeft
                                         : FloatingMenuPosition::kBottomLeft;
      break;
  }
  Shell::Get()->accessibility_controller()->SetFloatingMenuPosition(
      new_position);
  Shell::Get()
      ->accessibility_controller()
      ->TriggerAccessibilityAlertWithMessage(
          GetDescriptionForMovedToPosition(new_position));
}

void FloatingAccessibilityView::OnViewVisibilityChanged(
    views::View* observed_view,
    views::View* starting_view) {
  if (observed_view != starting_view)
    return;
  delegate_->OnLayoutChanged();
}

void FloatingAccessibilityView::OnFocusLeavingSystemTray(bool reverse) {}

void FloatingAccessibilityView::OnImeMenuTrayBubbleShown() {
  delegate_->OnDetailedMenuEnabled(false);
}

void FloatingAccessibilityView::OnKeyboardVisibilityChanged(bool visible) {
  // To avoid the collision with the virtual keyboard
  // Accessibility tray is closed after opening the virtual keyboard tray
  if (visible)
    delegate_->OnDetailedMenuEnabled(false);
}

BEGIN_METADATA(FloatingAccessibilityView)
END_METADATA

}  // namespace ash