chromium/chrome/browser/ash/input_method/assistive_window_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 "chrome/browser/ash/input_method/assistive_window_controller.h"

#include <string>
#include <vector>

#include "ash/constants/ash_features.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/shell.h"
#include "ash/wm/window_util.h"
#include "chrome/browser/ash/input_method/assistive_window_controller_delegate.h"
#include "chrome/browser/ash/input_method/assistive_window_properties.h"
#include "chrome/browser/ui/ash/input_method/suggestion_details.h"
#include "chrome/grit/generated_resources.h"
#include "ui/base/ime/ash/ime_bridge.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/views/widget/widget.h"

namespace ash {
namespace input_method {
namespace {

constexpr char16_t kAnnouncementViewName[] = u"Assistive Input";
constexpr base::TimeDelta kAnnouncementDelay = base::Milliseconds(100);
constexpr base::TimeDelta kShowSuggestionDelay = base::Milliseconds(5);

gfx::NativeView GetParentView() {
  gfx::NativeView parent = gfx::NativeView();

  aura::Window* active_window = ash::window_util::GetActiveWindow();
  // Use MenuContainer so that it works even with a system modal dialog.
  parent = ash::Shell::GetContainer(
      active_window ? active_window->GetRootWindow()
                    : ash::Shell::GetRootWindowForNewWindows(),
      ash::kShellWindowId_MenuContainer);
  return parent;
}

}  // namespace

AssistiveWindowController::AssistiveWindowController(
    AssistiveWindowControllerDelegate* delegate,
    Profile* profile,
    ui::ime::AnnouncementView* announcement_view)
    : delegate_(delegate), announcement_view_(announcement_view) {}

AssistiveWindowController::~AssistiveWindowController() {
  ClearPendingSuggestionTimer();
  if (suggestion_window_view_ && suggestion_window_view_->GetWidget()) {
    suggestion_window_view_->GetWidget()->RemoveObserver(this);
  }
  if (undo_window_ && undo_window_->GetWidget()) {
    undo_window_->GetWidget()->RemoveObserver(this);
  }
  if (grammar_suggestion_window_ && grammar_suggestion_window_->GetWidget()) {
    grammar_suggestion_window_->GetWidget()->RemoveObserver(this);
  }
  if (announcement_view_ && announcement_view_->GetWidget()) {
    announcement_view_->GetWidget()->RemoveObserver(this);
  }
  CHECK(!IsInObserverList());
}

void AssistiveWindowController::InitSuggestionWindow(
    ui::ime::SuggestionWindowView::Orientation orientation) {
  if (suggestion_window_view_) {
    return;
  }
  // suggestion_window_view_ is deleted by DialogDelegateView::DeleteDelegate.
  suggestion_window_view_ =
      ui::ime::SuggestionWindowView::Create(GetParentView(), this, orientation);
  views::Widget* widget = suggestion_window_view_->GetWidget();
  widget->AddObserver(this);
  widget->Show();
}

void AssistiveWindowController::InitUndoWindow() {
  if (undo_window_) {
    return;
  }
  // undo_window is deleted by DialogDelegateView::DeleteDelegate.
  undo_window_ = new ui::ime::UndoWindow(GetParentView(), this);
  views::Widget* widget = undo_window_->InitWidget();
  widget->AddObserver(this);
  widget->Show();
}

void AssistiveWindowController::InitGrammarSuggestionWindow() {
  if (grammar_suggestion_window_) {
    return;
  }
  // grammar_suggestion_window_ is deleted by
  // DialogDelegateView::DeleteDelegate.
  grammar_suggestion_window_ =
      new ui::ime::GrammarSuggestionWindow(GetParentView(), this);
  views::Widget* widget = grammar_suggestion_window_->InitWidget();
  widget->AddObserver(this);
  widget->Show();
}

void AssistiveWindowController::InitAnnouncementView() {
  if (announcement_view_) {
    return;
  }

  // accessibility_view_ is deleted by DialogDelegateView::DeleteDelegate.
  announcement_view_ =
      new ui::ime::AnnouncementView(GetParentView(), kAnnouncementViewName);
  announcement_view_->GetWidget()->AddObserver(this);
}

void AssistiveWindowController::OnWidgetDestroying(views::Widget* widget) {
  ClearPendingSuggestionTimer();
  if (suggestion_window_view_ &&
      widget == suggestion_window_view_->GetWidget()) {
    widget->RemoveObserver(this);
    suggestion_window_view_ = nullptr;
  }
  if (undo_window_ && widget == undo_window_->GetWidget()) {
    widget->RemoveObserver(this);
    undo_window_ = nullptr;
  }
  if (grammar_suggestion_window_ &&
      widget == grammar_suggestion_window_->GetWidget()) {
    widget->RemoveObserver(this);
    grammar_suggestion_window_ = nullptr;
  }
  if (announcement_view_ && widget == announcement_view_->GetWidget()) {
    widget->RemoveObserver(this);
    announcement_view_ = nullptr;
  }
}

void AssistiveWindowController::Announce(const std::u16string& message) {
  if (!announcement_view_) {
    InitAnnouncementView();
  }

  // Announcements for assistive suggestions often collide with key press or
  // text update announcements from ChromeVox. By adding a very small delay
  // these collisions are *mostly* avoided.
  announcement_view_->AnnounceAfterDelay(message, kAnnouncementDelay);
}

// TODO(crbug.com/40145550): Update AcceptSuggestion signature (either use
// announce_string, or no string)
void AssistiveWindowController::AcceptSuggestion(
    const std::u16string& suggestion) {
  if (window_.type == ash::ime::AssistiveWindowType::kEmojiSuggestion) {
    Announce(l10n_util::GetStringUTF16(IDS_SUGGESTION_EMOJI_SUGGESTED));
  } else {
    Announce(l10n_util::GetStringUTF16(IDS_SUGGESTION_INSERTED));
  }
  HideSuggestion();
}

void AssistiveWindowController::HideSuggestion() {
  suggestion_text_.clear();
  confirmed_length_ = 0;
  if (suggestion_window_view_) {
    suggestion_window_view_->GetWidget()->Close();
  }
  if (grammar_suggestion_window_) {
    grammar_suggestion_window_->GetWidget()->Close();
  }
}

void AssistiveWindowController::SetBounds(const Bounds& bounds) {
  if (bounds == bounds_) {
    return;
  }

  bounds_ = bounds;
  if (suggestion_window_view_) {
    suggestion_window_view_->SetAnchorRect(bounds.caret);
  }
  if (grammar_suggestion_window_) {
    grammar_suggestion_window_->SetBounds(bounds_.caret);
  }
  if (pending_suggestion_timer_ && pending_suggestion_timer_->IsRunning()) {
    pending_suggestion_timer_->FireNow();
    pending_suggestion_timer_ = nullptr;
  }
}

void AssistiveWindowController::FocusStateChanged() {
  HideSuggestion();
  if (undo_window_) {
    undo_window_->Hide();
  }
}

void AssistiveWindowController::ShowSuggestion(
    const ui::ime::SuggestionDetails& details) {
  suggestion_text_ = details.text;
  confirmed_length_ = details.confirmed_length;
  // Delay the showing of a completion suggestion. This is required to solve
  // b/241321719, where we receive a ShowSuggestion call prior to a
  // corresponding SetBounds call. Delaying allows any relevant SetBounds calls
  // to be received before we show the suggestion to the user.
  ClearPendingSuggestionTimer();
  pending_suggestion_timer_ = std::make_unique<base::OneShotTimer>();
  pending_suggestion_timer_->Start(
      FROM_HERE, kShowSuggestionDelay,
      base::BindOnce(&AssistiveWindowController::DisplayCompletionSuggestion,
                     weak_ptr_factory_.GetWeakPtr(), details));
}

void AssistiveWindowController::SetButtonHighlighted(
    const ui::ime::AssistiveWindowButton& button,
    bool highlighted) {
  switch (button.window_type) {
    case ash::ime::AssistiveWindowType::kEmojiSuggestion:
    case ash::ime::AssistiveWindowType::kPersonalInfoSuggestion:
    case ash::ime::AssistiveWindowType::kMultiWordSuggestion:
    case ash::ime::AssistiveWindowType::kLongpressDiacriticsSuggestion:
      if (!suggestion_window_view_) {
        return;
      }

      suggestion_window_view_->SetButtonHighlighted(button, highlighted);
      break;
    case ash::ime::AssistiveWindowType::kLearnMore:
    case ash::ime::AssistiveWindowType::kUndoWindow:
      if (!undo_window_) {
        return;
      }

      undo_window_->SetButtonHighlighted(button, highlighted);
      break;
    case ash::ime::AssistiveWindowType::kGrammarSuggestion:
      if (!grammar_suggestion_window_) {
        return;
      }

      grammar_suggestion_window_->SetButtonHighlighted(button, highlighted);
      break;
    case ash::ime::AssistiveWindowType::kNone:
      break;
  }
  if (highlighted) {
    Announce(button.announce_string);
  }
}

std::u16string AssistiveWindowController::GetSuggestionText() const {
  return suggestion_text_;
}

size_t AssistiveWindowController::GetConfirmedLength() const {
  return confirmed_length_;
}

ui::ime::SuggestionWindowView::Orientation
AssistiveWindowController::WindowOrientationFor(
    ash::ime::AssistiveWindowType window_type) {
  switch (window_type) {
    case ash::ime::AssistiveWindowType::kLongpressDiacriticsSuggestion:
    case ash::ime::AssistiveWindowType::kLearnMore:
      return ui::ime::SuggestionWindowView::Orientation::kHorizontal;
    case ash::ime::AssistiveWindowType::kUndoWindow:
    case ash::ime::AssistiveWindowType::kEmojiSuggestion:
    case ash::ime::AssistiveWindowType::kPersonalInfoSuggestion:
    case ash::ime::AssistiveWindowType::kMultiWordSuggestion:
    case ash::ime::AssistiveWindowType::kGrammarSuggestion:
      return ui::ime::SuggestionWindowView::Orientation::kVertical;
    case ash::ime::AssistiveWindowType::kNone:
      NOTREACHED_IN_MIGRATION();
  }
  NOTREACHED_IN_MIGRATION();
  return ui::ime::SuggestionWindowView::Orientation::kVertical;
}

void AssistiveWindowController::SetAssistiveWindowProperties(
    const AssistiveWindowProperties& window) {
  window_ = window;

  // Make sure any pending timers are cleared before we attempt to show, or
  // update, another assistive window.
  ClearPendingSuggestionTimer();

  switch (window.type) {
    case ash::ime::AssistiveWindowType::kUndoWindow:
      if (!undo_window_) {
        InitUndoWindow();
      }
      if (window.visible) {
        // Apply 4px padding to move the window away from the cursor.
        gfx::Rect anchor_rect =
            bounds_.autocorrect.IsEmpty() ? bounds_.caret : bounds_.autocorrect;
        anchor_rect.Inset(-4);
        undo_window_->SetAnchorRect(anchor_rect);
        undo_window_->Show(window.show_setting_link);
      } else {
        undo_window_->Hide();
      }
      break;
    case ash::ime::AssistiveWindowType::kLearnMore:
    case ash::ime::AssistiveWindowType::kEmojiSuggestion:
    case ash::ime::AssistiveWindowType::kPersonalInfoSuggestion:
    case ash::ime::AssistiveWindowType::kMultiWordSuggestion:
    case ash::ime::AssistiveWindowType::kLongpressDiacriticsSuggestion:
      if (!suggestion_window_view_) {
        InitSuggestionWindow(WindowOrientationFor(window.type));
      }
      if (window_.visible) {
        suggestion_window_view_->SetAnchorRect(bounds_.caret);
        suggestion_window_view_->ShowMultipleCandidates(
            window, WindowOrientationFor(window.type));
      } else {
        HideSuggestion();
      }
      break;
    case ash::ime::AssistiveWindowType::kGrammarSuggestion:
      if (window.candidates.size() == 0) {
        return;
      }
      if (!grammar_suggestion_window_) {
        InitGrammarSuggestionWindow();
      }
      if (window.visible) {
        grammar_suggestion_window_->SetBounds(bounds_.caret);
        grammar_suggestion_window_->SetSuggestion(window.candidates[0]);
        grammar_suggestion_window_->Show();
      } else {
        grammar_suggestion_window_->Hide();
      }
      break;
    case ash::ime::AssistiveWindowType::kNone:
      break;
  }
  Announce(window.announce_string);
}

void AssistiveWindowController::DisplayCompletionSuggestion(
    const ui::ime::SuggestionDetails& details) {
  if (!suggestion_window_view_) {
    InitSuggestionWindow(ui::ime::SuggestionWindowView::Orientation::kVertical);
  }
  suggestion_window_view_->SetAnchorRect(bounds_.caret);
  suggestion_window_view_->Show(details);
}

void AssistiveWindowController::ClearPendingSuggestionTimer() {
  if (pending_suggestion_timer_) {
    if (pending_suggestion_timer_->IsRunning()) {
      pending_suggestion_timer_->Stop();
    }
    pending_suggestion_timer_ = nullptr;
  }
}

void AssistiveWindowController::AssistiveWindowButtonClicked(
    const ui::ime::AssistiveWindowButton& button) const {
  delegate_->AssistiveWindowButtonClicked(button);
}

void AssistiveWindowController::AssistiveWindowChanged(
    const ash::ime::AssistiveWindow& window) const {
  delegate_->AssistiveWindowChanged(window);
}

ui::ime::SuggestionWindowView*
AssistiveWindowController::GetSuggestionWindowViewForTesting() {
  return suggestion_window_view_;
}

ui::ime::UndoWindow* AssistiveWindowController::GetUndoWindowForTesting()
    const {
  return undo_window_;
}

}  // namespace input_method
}  // namespace ash