blob: 0a281fcd93c01c5b45be8f643667be52f7ce6315 [file] [log] [blame]
// Copyright 2018 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/assistant/ui/assistant_notification_view.h"
#include "ash/assistant/ui/assistant_ui_constants.h"
#include "ash/assistant/ui/assistant_view_delegate.h"
#include "ash/assistant/ui/main_stage/suggestion_chip_view.h"
#include "base/strings/utf_string_conversions.h"
#include "chromeos/services/assistant/public/mojom/assistant.mojom.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/views/animation/ink_drop_painted_layer_delegates.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
namespace ash {
namespace {
// Appearance.
constexpr int kLineHeightDip = 20;
constexpr int kPaddingLeftDip = 16;
constexpr int kPaddingRightDip = 8;
constexpr int kPreferredHeightDip = 48;
constexpr int kShadowElevationDip = 6;
// Animation.
constexpr base::TimeDelta kAnimationDuration =
base::TimeDelta::FromMilliseconds(250);
// Helpers ---------------------------------------------------------------------
views::View* CreateButton(
const chromeos::assistant::mojom::AssistantNotificationButtonPtr& button,
views::ButtonListener* listener) {
SuggestionChipView::Params params;
params.text = base::UTF8ToUTF16(button->label);
return new SuggestionChipView(params, listener);
}
} // namespace
AssistantNotificationView::AssistantNotificationView(
AssistantViewDelegate* delegate,
const AssistantNotification* notification)
: delegate_(delegate), notification_id_(notification->client_id) {
InitLayout(notification);
// The AssistantViewDelegate outlives the Assistant view hierarchy.
delegate_->AddNotificationModelObserver(this);
}
AssistantNotificationView::~AssistantNotificationView() {
delegate_->RemoveNotificationModelObserver(this);
}
const char* AssistantNotificationView::GetClassName() const {
return "AssistantNotificationView";
}
void AssistantNotificationView::AddedToWidget() {
UpdateVisibility(/*visible=*/true);
}
gfx::Size AssistantNotificationView::CalculatePreferredSize() const {
return gfx::Size(INT_MAX, GetHeightForWidth(INT_MAX));
}
int AssistantNotificationView::GetHeightForWidth(int width) const {
return kPreferredHeightDip;
}
void AssistantNotificationView::OnBoundsChanged(
const gfx::Rect& previous_bounds) {
UpdateBackground();
}
void AssistantNotificationView::ButtonPressed(views::Button* sender,
const ui::Event& event) {
const auto it = std::find(buttons_.begin(), buttons_.end(), sender);
const int notification_button_index = std::distance(buttons_.begin(), it);
delegate_->OnNotificationButtonPressed(notification_id_,
notification_button_index);
}
void AssistantNotificationView::OnImplicitAnimationsCompleted() {
// When the view's layer has animated to |0.f| opacity, the underlying
// notification has been removed and our associated view can be deleted. Note
// that we check for opacity within epsilon to avoid exact float comparison.
if (cc::MathUtil::IsWithinEpsilon(layer()->opacity(), 0.f))
delete this;
}
void AssistantNotificationView::OnNotificationUpdated(
const AssistantNotification* notification) {
// We only care about the |notification| being updated if it is the
// notification associated with this view.
if (notification->client_id != notification_id_)
return;
using AssistantNotificationType =
chromeos::assistant::mojom::AssistantNotificationType;
// If the notification associated with this view is no longer of type
// |kInAssistant|, it should not be shown in Assistant UI.
if (notification->type != AssistantNotificationType::kInAssistant) {
UpdateVisibility(/*visible=*/false);
return;
}
// Title/Message.
title_->SetText(base::UTF8ToUTF16(notification->title));
message_->SetText(base::UTF8ToUTF16(notification->message));
// Old buttons.
for (views::View* button : buttons_)
delete button;
buttons_.clear();
// New buttons.
for (const auto& notification_button : notification->buttons) {
views::View* button = CreateButton(notification_button, /*listener=*/this);
container_->AddChildView(button);
buttons_.push_back(button);
}
// Because |container_| has a fixed size, we need to explicitly trigger a
// layout/paint pass ourselves when manipulating child views.
container_->Layout();
container_->SchedulePaint();
}
void AssistantNotificationView::OnNotificationRemoved(
const AssistantNotification* notification,
bool from_server) {
if (notification->client_id == notification_id_)
UpdateVisibility(/*visible=*/false);
}
void AssistantNotificationView::InitLayout(
const AssistantNotification* notification) {
SetLayoutManager(std::make_unique<views::FillLayout>());
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
// Initialize opacity to |0.f| as the layer for this view will be animated in
// when the view is added to its widget.
layer()->SetOpacity(0.f);
// Background/shadow.
background_layer_.SetFillsBoundsOpaquely(false);
layer()->Add(&background_layer_);
// Container.
container_ = new views::View();
container_->SetPreferredSize(gfx::Size(INT_MAX, INT_MAX));
container_->SetPaintToLayer();
container_->layer()->SetFillsBoundsOpaquely(false);
AddChildView(container_);
auto* layout_manager =
container_->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal,
gfx::Insets(0, kPaddingLeftDip, 0, kPaddingRightDip), kSpacingDip));
layout_manager->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kCenter);
gfx::FontList font_list =
assistant::ui::GetDefaultFontList().DeriveWithSizeDelta(1);
// Title.
title_ = new views::Label(base::UTF8ToUTF16(notification->title));
title_->SetAutoColorReadabilityEnabled(false);
title_->SetEnabledColor(kTextColorPrimary);
title_->SetFontList(font_list.DeriveWithWeight(gfx::Font::Weight::MEDIUM));
title_->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
title_->SetLineHeight(kLineHeightDip);
container_->AddChildView(title_);
// Message.
message_ = new views::Label(base::UTF8ToUTF16(notification->message));
message_->SetAutoColorReadabilityEnabled(false);
message_->SetEnabledColor(kTextColorSecondary);
message_->SetFontList(font_list);
message_->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
message_->SetLineHeight(kLineHeightDip);
container_->AddChildView(message_);
layout_manager->SetFlexForView(message_, 1);
// Buttons.
for (const auto& notification_button : notification->buttons) {
views::View* button = CreateButton(notification_button, /*listener=*/this);
container_->AddChildView(button);
buttons_.push_back(button);
}
}
void AssistantNotificationView::UpdateBackground() {
gfx::ShadowValues shadow_values =
gfx::ShadowValue::MakeMdShadowValues(kShadowElevationDip);
shadow_delegate_ = std::make_unique<views::BorderShadowLayerDelegate>(
shadow_values, GetLocalBounds(),
/*fill_color=*/SK_ColorWHITE,
/*corner_radius=*/height() / 2);
background_layer_.set_delegate(shadow_delegate_.get());
background_layer_.SetBounds(
gfx::ToEnclosingRect(shadow_delegate_->GetPaintedBounds()));
}
void AssistantNotificationView::UpdateVisibility(bool visible) {
ui::ScopedLayerAnimationSettings animation(layer()->GetAnimator());
// We observe the animation to receive an event on its completion. When the
// layer for this view has completed animating to a hidden state, this view
// is deleted as the underlying notification has been removed.
animation.AddObserver(this);
// Parameters.
animation.SetPreemptionStrategy(
ui::LayerAnimator::PreemptionStrategy::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
animation.SetTransitionDuration(kAnimationDuration);
animation.SetTweenType(gfx::Tween::Type::EASE_IN_OUT);
// Animate opacity to a visible/hidden state.
layer()->SetOpacity(visible ? 1.f : 0.f);
}
} // namespace ash