chromium/ash/assistant/ui/main_stage/assistant_card_element_view.cc

// Copyright 2018 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/assistant/ui/main_stage/assistant_card_element_view.h"

#include <memory>
#include <tuple>

#include "ash/assistant/model/ui/assistant_card_element.h"
#include "ash/assistant/ui/assistant_ui_constants.h"
#include "ash/assistant/ui/assistant_view_delegate.h"
#include "ash/assistant/ui/main_stage/assistant_ui_element_view_animator.h"
#include "ash/assistant/util/deep_link_util.h"
#include "ash/public/cpp/assistant/controller/assistant_controller.h"
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/events/event.h"
#include "ui/events/event_sink.h"
#include "ui/events/event_utils.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"

namespace ash {

namespace {

using assistant::util::DeepLinkParam;
using assistant::util::DeepLinkType;

constexpr char kAssistantCardElementHistogram[] =
    "Ash.Assistant.AnimationSmoothness.CardElement";

// Helpers ---------------------------------------------------------------------

void CreateAndSendMouseClick(aura::WindowTreeHost* host,
                             const gfx::Point& location_in_pixels) {
  ui::MouseEvent press_event(ui::EventType::kMousePressed, location_in_pixels,
                             location_in_pixels, ui::EventTimeForNow(),
                             ui::EF_LEFT_MOUSE_BUTTON,
                             ui::EF_LEFT_MOUSE_BUTTON);

  // Send an kMousePressed event.
  ui::EventDispatchDetails details =
      host->GetEventSink()->OnEventFromSource(&press_event);

  if (details.dispatcher_destroyed)
    return;

  ui::MouseEvent release_event(ui::EventType::kMouseReleased,
                               location_in_pixels, location_in_pixels,
                               ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON,
                               ui::EF_LEFT_MOUSE_BUTTON);

  // Send an EventType::kMouseReleased event.
  std::ignore = host->GetEventSink()->OnEventFromSource(&release_event);
}

}  // namespace

AssistantCardElementView::AssistantCardElementView(
    AssistantViewDelegate* delegate,
    const AssistantCardElement* card_element)
    : delegate_(delegate), card_element_(card_element) {
  InitLayout();

  // We observe contents_view() to receive events pertaining to the underlying
  // WebContents including focus change and suppressed navigation events.
  contents_view_->AddObserver(this);
}

AssistantCardElementView::~AssistantCardElementView() {
  contents_view_->RemoveObserver(this);
}

ui::Layer* AssistantCardElementView::GetLayerForAnimating() {
  // native_view() can be nullptr if this runs in unit test.
  if (!native_view())
    return nullptr;

  return native_view()->layer();
}

std::string AssistantCardElementView::ToStringForTesting() const {
  return card_element_->html();
}

void AssistantCardElementView::AddedToWidget() {
  // native_view() can be nullptr if this runs in unit test.
  if (!native_view())
    return;

  aura::Window* const top_level_window = native_view()->GetToplevelWindow();

  // Find the window for the Assistant card.
  aura::Window* window = native_view();
  while (window->parent() != top_level_window)
    window = window->parent();

  // The Assistant card window will consume all events that enter it. This
  // prevents us from being able to scroll the native view hierarchy
  // vertically. As such, we need to prevent the Assistant card window from
  // receiving events it doesn't need. It needs mouse click events for
  // handling links.
  window->SetProperty(assistant::ui::kOnlyAllowMouseClickEvents, true);
}

void AssistantCardElementView::ChildPreferredSizeChanged(views::View* child) {
  PreferredSizeChanged();
}

void AssistantCardElementView::OnGestureEvent(ui::GestureEvent* event) {
  // We need to route GESTURE_TAP events to our Assistant card because links
  // should be tappable. The Assistant card window will not receive gesture
  // events so we convert the gesture into analogous mouse events.
  if (event->type() != ui::EventType::kGestureTap) {
    views::View::OnGestureEvent(event);
    return;
  }

  // Consume the original event.
  event->StopPropagation();
  event->SetHandled();

  aura::Window* root_window = GetWidget()->GetNativeWindow()->GetRootWindow();

  // Get the appropriate event location in pixels.
  gfx::Point location_in_pixels = event->location();
  ConvertPointToScreen(this, &location_in_pixels);
  aura::WindowTreeHost* host = root_window->GetHost();
  host->ConvertDIPToPixels(&location_in_pixels);

  wm::CursorManager* cursor_manager = delegate_->GetCursorManager();

  // We want to prevent the cursor from changing its visibility during our
  // mouse events because we are actually handling a gesture. To accomplish
  // this, we cache the cursor's visibility and lock it in its current state.
  const bool visible = cursor_manager->IsCursorVisible();
  cursor_manager->LockCursor();

  CreateAndSendMouseClick(host, location_in_pixels);

  // Restore the original cursor visibility that may have changed during our
  // sequence of mouse events. This change would not have been perceivable to
  // the user since it occurred within our lock.
  if (visible)
    cursor_manager->ShowCursor();
  else
    cursor_manager->HideCursor();

  // Release our cursor lock.
  cursor_manager->UnlockCursor();
}

void AssistantCardElementView::ScrollRectToVisible(const gfx::Rect& rect) {
  // We expect this method is called outside this class to show its contents
  // bounds. Inside this class, should call views::View::ScrollRectToVisible()
  // to show the focused node in the web contents.
  DCHECK(rect == GetContentsBounds());

  // When this view is focused, View::Focus() calls ScrollViewToVisible(), which
  // calls ScrollRectToVisible().  But we don't want that call to do anything,
  // since the true focused item is not this view but a node in the contained
  // web contents.  That will be scrolled into view by FocusedNodeChanged()
  // below, so just no-op here.
  if (focused_node_rect_.IsEmpty())
    return;

  // Make the focused node visible.
  views::View::ScrollRectToVisible(focused_node_rect_);
}

void AssistantCardElementView::DidSuppressNavigation(
    const GURL& url,
    WindowOpenDisposition disposition,
    bool from_user_gesture) {
  // We delegate navigation to the AssistantController so that it can apply
  // special handling to deep links.
  AssistantController::Get()->OpenUrl(url);
}

void AssistantCardElementView::DidChangeFocusedNode(
    const gfx::Rect& node_bounds_in_screen) {
  // TODO(b/143985066): Card has element with empty bounds, e.g. the line break.
  if (node_bounds_in_screen.IsEmpty())
    return;

  gfx::Point origin = node_bounds_in_screen.origin();
  ConvertPointFromScreen(this, &origin);
  focused_node_rect_ = gfx::Rect(origin, node_bounds_in_screen.size());
  views::View::ScrollRectToVisible(focused_node_rect_);
}

void AssistantCardElementView::InitLayout() {
  SetLayoutManager(std::make_unique<views::FillLayout>());

  // Contents view.
  contents_view_ =
      AddChildView(const_cast<AssistantCardElement*>(card_element_.get())
                       ->MoveContentsView());
}

std::unique_ptr<ElementAnimator> AssistantCardElementView::CreateAnimator() {
  return std::make_unique<AssistantUiElementViewAnimator>(
      this, kAssistantCardElementHistogram);
}

BEGIN_METADATA(AssistantCardElementView)
END_METADATA

}  // namespace ash