blob: c233be08ace2e4977055fa3a4748f14b3dea3bec [file] [log] [blame]
// Copyright 2021 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/time/calendar_view.h"
#include <memory>
#include <string>
#include "ash/bubble/bubble_utils.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/ash_typography.h"
#include "ash/public/cpp/ash_view_ids.h"
#include "ash/public/cpp/metrics_util.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/icon_button.h"
#include "ash/style/pill_button.h"
#include "ash/style/typography.h"
#include "ash/system/time/calendar_event_list_view.h"
#include "ash/system/time/calendar_metrics.h"
#include "ash/system/time/calendar_month_view.h"
#include "ash/system/time/calendar_up_next_view.h"
#include "ash/system/time/calendar_utils.h"
#include "ash/system/time/calendar_view_controller.h"
#include "ash/system/time/date_helper.h"
#include "ash/system/tray/tray_popup_utils.h"
#include "ash/system/tray/tri_view.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/compositor/animation_throughput_reporter.h"
#include "ui/compositor/compositor.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/layer_type.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/compositor/presentation_time_recorder.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/transform.h"
#include "ui/gfx/geometry/vector2d_f.h"
#include "ui/gfx/interpolated_transform.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/scrollbar/scroll_bar.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/table_layout.h"
#include "ui/views/style/typography.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
namespace ash {
namespace {
// The paddings in each view.
constexpr int kContentVerticalPadding = 20;
constexpr int kContentHorizontalPadding = 20;
constexpr int kMonthVerticalPadding = 10;
constexpr int kLabelVerticalPadding = 10;
constexpr int kLabelTextInBetweenPadding = 10;
const int kWeekRowHorizontalPadding =
kContentHorizontalPadding - calendar_utils::kDateHorizontalPadding;
const int kWeekRowHorizontalPaddingJelly =
kContentHorizontalPadding - calendar_utils::kDateHorizontalPaddingJelly;
constexpr int kExpandedCalendarPadding = 11;
constexpr int kExpandedCalendarPaddingJelly = 10;
constexpr int kChevronPadding = calendar_utils::kColumnSetPadding - 1;
constexpr int kMonthHeaderLabelTopPadding = 14;
constexpr int kMonthHeaderLabelBottomPadding = 2;
constexpr int kEventListViewHorizontalOffset = 1;
constexpr int kUpNextAnimationYOffset = 20;
// Adds a gap between the bottom visible row in the scrollview and the top of
// the event list view when open.
constexpr int kCalendarEventListViewOpenMarginJelly = 8;
// The offset for `month_label_` to make it align with `month_header`.
constexpr int kMonthLabelPaddingOffset = -1;
// The cool-down time for calling `UpdateOnScreenMonthMap()` after scrolling.
constexpr base::TimeDelta kScrollingSettledTimeout = base::Milliseconds(500);
// The max number of rows in a month.
constexpr int kMaxRowsInOneMonth = 6;
// Duration of the delay for starting header animation.
constexpr base::TimeDelta kDelayHeaderAnimationDuration =
base::Milliseconds(200);
// Duration of events moving animation.
constexpr base::TimeDelta kAnimationDurationForEventsMoving =
base::Milliseconds(400);
// Duration of closing events panel animation.
constexpr base::TimeDelta kAnimationDurationForClosingEvents =
base::Milliseconds(200);
// Duration of `event_list_view_` fade in animation delay.
constexpr base::TimeDelta kEventListAnimationStartDelay =
base::Milliseconds(100);
// Duration of `up_next_view_` fade in animation delay.
constexpr base::TimeDelta kUpNextAnimationStartDelay = base::Milliseconds(50);
// The cool-down time for enabling animation.
constexpr base::TimeDelta kAnimationDisablingTimeout = base::Milliseconds(500);
// Periodic time delay for checking upcoming events.
constexpr base::TimeDelta kCheckUpcomingEventsDelay = base::Seconds(15);
// The multiplier used to reduce velocity of flings on the calendar view.
// Without this, CalendarView will scroll a few years per fast swipe.
constexpr float kCalendarScrollFlingMultiplier = 0.25f;
constexpr char kMonthViewScrollOneMonthAnimationHistogram[] =
"Ash.CalendarView.ScrollOneMonth.MonthView.AnimationSmoothness";
constexpr char kLabelViewScrollOneMonthAnimationHistogram[] =
"Ash.CalendarView.ScrollOneMonth.LabelView.AnimationSmoothness";
constexpr char kHeaderViewScrollOneMonthAnimationHistogram[] =
"Ash.CalendarView.ScrollOneMonth.HeaderView.AnimationSmoothness";
constexpr char kContentViewResetToTodayAnimationHistogram[] =
"Ash.CalendarView.ResetToToday.ContentView.AnimationSmoothness";
constexpr char kHeaderViewResetToTodayAnimationHistogram[] =
"Ash.CalendarView.ResetToToday.HeaderView.AnimationSmoothness";
constexpr char kContentViewFadeInCurrentMonthAnimationHistogram[] =
"Ash.CalendarView.FadeInCurrentMonth.ContentView.AnimationSmoothness";
constexpr char kHeaderViewFadeInCurrentMonthAnimationHistogram[] =
"Ash.CalendarView.FadeInCurrentMonth.HeaderView.AnimationSmoothness";
constexpr char kOnMonthChangedAnimationHistogram[] =
"Ash.CalendarView.OnMonthChanged.AnimationSmoothness";
constexpr char kCloseEventListAnimationHistogram[] =
"Ash.CalendarView.CloseEventList.EventListView.AnimationSmoothness";
constexpr char kCloseEventListCalendarSlidingSurfaceAnimationHistogram[] =
"Ash.CalendarView.CloseEventList.CalendarSlidingSurface."
"AnimationSmoothness";
constexpr char kCloseEventListUpNextViewAnimationHistogram[] =
"Ash.CalendarView.CloseEventList.UpNextView.AnimationSmoothness";
constexpr char kMonthViewOpenEventListAnimationHistogram[] =
"Ash.CalendarView.OpenEventList.MonthView.AnimationSmoothness";
constexpr char kLabelViewOpenEventListAnimationHistogram[] =
"Ash.CalendarView.OpenEventList.LabelView.AnimationSmoothness";
constexpr char kEventListViewOpenEventListAnimationHistogram[] =
"Ash.CalendarView.OpenEventList.EventListView.AnimationSmoothness";
constexpr char kCalendarSlidingSurfaceOpenEventListAnimationHistogram[] =
"Ash.CalendarView.OpenEventList.CalendarSlidingSurface.AnimationSmoothness";
constexpr char kUpNextViewOpenEventListAnimationHistogram[] =
"Ash.CalendarView.OpenEventList.UpNextView.AnimationSmoothness";
constexpr char kShowUpNextViewAnimationHistogram[] =
"Ash.CalendarView.ShowUpNextView.AnimationSmoothness";
constexpr char kSmoothScrollMonthViewWhenShowingTodaysDateCell[] =
"Ash.CalendarView.SmoothScrollToTodaysDateCell.MonthView."
"AnimationSmoothness";
constexpr char kSmoothScrollLabelViewWhenShowingTodaysDateCell[] =
"Ash.CalendarView.SmoothScrollToTodaysDateCell.LabelView."
"AnimationSmoothness";
std::unique_ptr<views::Label> HeaderView(const std::u16string& month) {
return views::Builder<views::Label>(
bubble_utils::CreateLabel(TypographyToken::kCrosDisplay7, month,
cros_tokens::kCrosSysOnSurface))
.SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_TO_HEAD)
.SetTextContext(CONTEXT_CALENDAR_LABEL)
.SetAutoColorReadabilityEnabled(false)
.Build();
}
int GetExpandedCalendarPadding() {
return features::IsCalendarJellyEnabled() ? kExpandedCalendarPaddingJelly
: kExpandedCalendarPadding;
}
// The overridden `Label` view used in `CalendarView`.
class CalendarLabel : public views::Label {
public:
explicit CalendarLabel(const std::u16string& text) : views::Label(text) {
views::Label::SetEnabledColor(calendar_utils::GetPrimaryTextColor());
views::Label::SetAutoColorReadabilityEnabled(false);
}
CalendarLabel(const CalendarLabel&) = delete;
CalendarLabel& operator=(const CalendarLabel&) = delete;
~CalendarLabel() override = default;
void OnThemeChanged() override {
views::Label::OnThemeChanged();
views::Label::SetEnabledColor(calendar_utils::GetPrimaryTextColor());
}
};
// The month view header which contains the title of each week day.
class MonthHeaderView : public views::View {
public:
MonthHeaderView() {
views::TableLayout* layout =
SetLayoutManager(std::make_unique<views::TableLayout>());
calendar_utils::SetUpWeekColumns(layout);
layout->AddRows(1, views::TableLayout::kFixedSize);
for (const std::u16string& week_day :
DateHelper::GetInstance()->week_titles()) {
auto label = features::IsCalendarJellyEnabled()
? views::Builder<views::Label>(
bubble_utils::CreateLabel(
TypographyToken::kCrosButton1, week_day,
cros_tokens::kCrosSysOnSurface))
.Build()
: std::make_unique<CalendarLabel>(week_day);
label->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_CENTER);
label->SetBorder((views::CreateEmptyBorder(
gfx::Insets::VH(calendar_utils::kDateVerticalPadding, 0))));
label->SetElideBehavior(gfx::NO_ELIDE);
label->SetSubpixelRenderingEnabled(false);
if (!features::IsCalendarJellyEnabled()) {
label->SetFontList(views::style::GetFont(
CONTEXT_CALENDAR_DATE, views::style::STYLE_EMPHASIZED));
}
AddChildView(std::move(label));
}
}
MonthHeaderView(const MonthHeaderView& other) = delete;
MonthHeaderView& operator=(const MonthHeaderView& other) = delete;
~MonthHeaderView() override = default;
};
// Resets the `view`'s opacity and position.
void ResetLayer(views::View* view) {
view->layer()->SetOpacity(1.0f);
view->layer()->SetTransform(gfx::Transform());
}
} // namespace
// The label for each month that's within the scroll view.
class CalendarView::MonthHeaderLabelView : public views::View {
public:
MonthHeaderLabelView(LabelType type,
CalendarViewController* calendar_view_controller)
: month_label_(AddChildView(HeaderView(std::u16string()))) {
// The layer is required in animation.
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
switch (type) {
case PREVIOUS:
month_name_ = calendar_view_controller->GetPreviousMonthName();
break;
case CURRENT:
month_name_ = calendar_view_controller->GetOnScreenMonthName();
break;
case NEXT:
month_name_ = calendar_view_controller->GetNextMonthName();
break;
case NEXTNEXT:
month_name_ =
calendar_view_controller->GetNextMonthName(/*num_months=*/2);
break;
}
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal));
month_label_->SetText(month_name_);
month_label_->SetBorder(views::CreateEmptyBorder(
gfx::Insets::TLBR(kMonthHeaderLabelTopPadding,
kContentHorizontalPadding + kMonthLabelPaddingOffset,
kMonthHeaderLabelBottomPadding, 0)));
}
MonthHeaderLabelView(const MonthHeaderLabelView&) = delete;
MonthHeaderLabelView& operator=(const MonthHeaderLabelView&) = delete;
~MonthHeaderLabelView() override = default;
// views::View:
void OnThemeChanged() override {
views::View::OnThemeChanged();
if (!features::IsCalendarJellyEnabled()) {
month_label_->SetEnabledColor(calendar_utils::GetPrimaryTextColor());
}
}
private:
// The name of the month.
std::u16string month_name_;
// The month label in the view.
const raw_ptr<views::Label, ExperimentalAsh> month_label_ = nullptr;
};
CalendarView::ScrollContentsView::ScrollContentsView(
CalendarViewController* controller)
: controller_(controller),
stylus_event_handler_(this),
current_month_(controller_->GetOnScreenMonthName()) {}
void CalendarView::ScrollContentsView::OnMonthChanged() {
current_month_ = controller_->GetOnScreenMonthName();
}
void CalendarView::ScrollContentsView::OnEvent(ui::Event* event) {
views::View::OnEvent(event);
if (controller_->GetOnScreenMonthName() == current_month_) {
return;
}
OnMonthChanged();
if (event->IsMouseWheelEvent()) {
calendar_metrics::RecordScrollSource(
calendar_metrics::CalendarViewScrollSource::kByMouseWheel);
}
if (event->IsScrollGestureEvent()) {
calendar_metrics::RecordScrollSource(
calendar_metrics::CalendarViewScrollSource::kByGesture);
}
if (event->IsFlingScrollEvent()) {
calendar_metrics::RecordScrollSource(
calendar_metrics::CalendarViewScrollSource::kByFling);
}
}
void CalendarView::ScrollContentsView::OnStylusEvent(
const ui::TouchEvent& event) {
if (controller_->GetOnScreenMonthName() == current_month_) {
return;
}
OnMonthChanged();
calendar_metrics::RecordScrollSource(
calendar_metrics::CalendarViewScrollSource::kByStylus);
}
CalendarView::ScrollContentsView::StylusEventHandler::StylusEventHandler(
ScrollContentsView* content_view)
: content_view_(content_view) {
Shell::Get()->AddPreTargetHandler(this);
}
CalendarView::ScrollContentsView::StylusEventHandler::~StylusEventHandler() {
Shell::Get()->RemovePreTargetHandler(this);
}
void CalendarView::ScrollContentsView::StylusEventHandler::OnTouchEvent(
ui::TouchEvent* event) {
if (event->pointer_details().pointer_type == ui::EventPointerType::kPen) {
content_view_->OnStylusEvent(*event);
}
}
CalendarHeaderView::CalendarHeaderView(const std::u16string& month,
const std::u16string& year)
: header_(AddChildView(HeaderView(month))),
header_year_(AddChildView(
views::Builder<views::Label>(
bubble_utils::CreateLabel(TypographyToken::kCrosDisplay7,
year,
cros_tokens::kCrosSysOnSurface))
.SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_TO_HEAD)
.SetTextContext(CONTEXT_CALENDAR_LABEL)
.SetAutoColorReadabilityEnabled(false)
.SetBorder(views::CreateEmptyBorder(
gfx::Insets::TLBR(0,
kLabelTextInBetweenPadding,
0,
kLabelTextInBetweenPadding)))
.Build())) {
// The layer is required in animation.
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal));
}
CalendarHeaderView::~CalendarHeaderView() = default;
void CalendarHeaderView::OnThemeChanged() {
views::View::OnThemeChanged();
if (!features::IsCalendarJellyEnabled()) {
header_->SetEnabledColor(calendar_utils::GetPrimaryTextColor());
header_year_->SetEnabledColor(calendar_utils::GetSecondaryTextColor());
}
}
void CalendarHeaderView::UpdateHeaders(const std::u16string& month,
const std::u16string& year) {
header_->SetText(month);
header_year_->SetText(year);
}
BEGIN_METADATA(CalendarHeaderView, views::View)
END_METADATA
CalendarView::CalendarView(DetailedViewDelegate* delegate,
UnifiedSystemTrayController* controller)
: TrayDetailedView(delegate),
controller_(controller),
calendar_view_controller_(std::make_unique<CalendarViewController>()),
scrolling_settled_timer_(
FROM_HERE,
kScrollingSettledTimeout,
base::BindRepeating(&CalendarView::UpdateOnScreenMonthMap,
base::Unretained(this))),
header_animation_restart_timer_(
FROM_HERE,
kAnimationDisablingTimeout,
base::BindRepeating(
[](CalendarView* calendar_view) {
if (!calendar_view) {
return;
}
calendar_view->set_should_header_animate(true);
},
base::Unretained(this))),
months_animation_restart_timer_(
FROM_HERE,
kAnimationDisablingTimeout,
base::BindRepeating(
[](CalendarView* calendar_view) {
if (!calendar_view) {
return;
}
calendar_view->set_should_months_animate(true);
},
base::Unretained(this))) {
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical));
SetFocusBehavior(FocusBehavior::ALWAYS);
// Focusable nodes must have an accessible name and valid role.
// TODO(crbug.com/1348930): Review the accessible name and role.
GetViewAccessibility().OverrideRole(ax::mojom::Role::kPane);
GetViewAccessibility().OverrideName(GetClassName());
// Since there's no separator in the `CalendarView`, first sets
// `has_separator` in `TrayDetailedView` to false.
IgnoreSeparator();
CreateTitleRow(IDS_ASH_CALENDAR_TITLE, /*create_back_button=*/false);
// Adds the progress bar to layout when initialization to avoid changing the
// layout while reading the bounds of it.
ShowProgress(-1, false);
// Add the header. The `temp_header_` only shows up during the header
// animation.
auto* header_container = new views::View();
header_container->SetLayoutManager(std::make_unique<views::FillLayout>());
auto header = std::make_unique<CalendarHeaderView>(
calendar_view_controller_->GetOnScreenMonthName(),
calendar_utils::GetYear(
calendar_view_controller_->currently_shown_date()));
auto temp_header = std::make_unique<CalendarHeaderView>(
calendar_view_controller_->GetPreviousMonthName(),
calendar_utils::GetYear(
calendar_view_controller_->currently_shown_date()));
temp_header->SetVisible(false);
header_ = header_container->AddChildView(std::move(header));
temp_header_ = header_container->AddChildView(std::move(temp_header));
TriView* tri_view =
TrayPopupUtils::CreateDefaultRowView(/*use_wide_layout=*/false);
tri_view->SetBorder(views::CreateEmptyBorder(
gfx::Insets::TLBR(kLabelVerticalPadding, kContentHorizontalPadding, 0,
kContentHorizontalPadding - kChevronPadding)));
tri_view->AddView(TriView::Container::START, header_container);
auto* button_container = new views::View();
const int horizontal_padding = features::IsCalendarJellyEnabled()
? kWeekRowHorizontalPaddingJelly
: kWeekRowHorizontalPadding;
views::BoxLayout* button_container_layout =
button_container->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal));
button_container_layout->set_main_axis_alignment(
views::BoxLayout::MainAxisAlignment::kEnd);
// Aligns button with the calendar dates in the `TableLayout`.
button_container_layout->set_between_child_spacing(horizontal_padding +
kChevronPadding);
up_button_ = button_container->AddChildView(std::make_unique<IconButton>(
base::BindRepeating(&CalendarView::OnMonthArrowButtonActivated,
base::Unretained(this), /*up=*/true),
IconButton::Type::kMediumFloating, &vector_icons::kCaretUpIcon,
IDS_ASH_CALENDAR_UP_BUTTON_ACCESSIBLE_DESCRIPTION));
down_button_ = button_container->AddChildView(std::make_unique<IconButton>(
base::BindRepeating(&CalendarView::OnMonthArrowButtonActivated,
base::Unretained(this), /*up=*/false),
IconButton::Type::kMediumFloating, &vector_icons::kCaretDownIcon,
IDS_ASH_CALENDAR_DOWN_BUTTON_ACCESSIBLE_DESCRIPTION));
tri_view->AddView(TriView::Container::END, button_container);
AddChildView(tri_view);
// Add month header.
auto month_header = std::make_unique<MonthHeaderView>();
month_header->SetBorder(views::CreateEmptyBorder(
gfx::Insets::TLBR(0, horizontal_padding, 0, horizontal_padding)));
AddChildView(std::move(month_header));
// Add scroll view.
scroll_view_ = AddChildView(std::make_unique<views::ScrollView>());
scroll_view_->SetAllowKeyboardScrolling(false);
scroll_view_->SetBackgroundColor(absl::nullopt);
ClipScrollViewHeight(ScrollViewState::FULL_HEIGHT);
scroll_view_->SetDrawOverflowIndicator(false);
scroll_view_->SetVerticalScrollBarMode(
views::ScrollView::ScrollBarMode::kHiddenButEnabled);
scroll_view_->vertical_scroll_bar()->SetFlingMultiplier(
kCalendarScrollFlingMultiplier);
scroll_view_->SetFocusBehavior(FocusBehavior::NEVER);
on_contents_scrolled_subscription_ =
scroll_view_->AddContentsScrolledCallback(base::BindRepeating(
&CalendarView::OnContentsScrolled, base::Unretained(this)));
content_view_ = scroll_view_->SetContents(
std::make_unique<ScrollContentsView>(calendar_view_controller_.get()));
content_view_->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical));
content_view_->SetBorder(views::CreateEmptyBorder(
gfx::Insets::TLBR(kContentVerticalPadding, horizontal_padding,
kContentVerticalPadding, horizontal_padding)));
// Focusable nodes must have an accessible name and valid role.
// TODO(crbug.com/1348930): Review the accessible name and role.
content_view_->GetViewAccessibility().OverrideRole(ax::mojom::Role::kPane);
content_view_->GetViewAccessibility().OverrideName(GetClassName());
content_view_->SetFocusBehavior(FocusBehavior::ALWAYS);
// Set up layer for animations.
content_view_->SetPaintToLayer();
content_view_->layer()->SetFillsBoundsOpaquely(false);
SetMonthViews();
// Container used for animating the event list view and / or the up next view.
calendar_sliding_surface_ = AddChildView(std::make_unique<views::View>());
calendar_sliding_surface_->SetUseDefaultFillLayout(true);
// We manipulate this layer with translations which can take it off the screen
// so for the animations to work we need to control its positioning.
calendar_sliding_surface_->SetProperty(views::kViewIgnoredByLayoutKey, true);
// This layer is required for animations.
calendar_sliding_surface_->SetPaintToLayer();
calendar_sliding_surface_->layer()->SetFillsBoundsOpaquely(false);
scoped_calendar_model_observer_.Observe(calendar_model_.get());
scoped_calendar_view_controller_observer_.Observe(
calendar_view_controller_.get());
scoped_view_observer_.AddObservation(scroll_view_.get());
scoped_view_observer_.AddObservation(content_view_.get());
scoped_view_observer_.AddObservation(this);
check_upcoming_events_timer_.Start(
FROM_HERE, kCheckUpcomingEventsDelay,
base::BindRepeating(&CalendarView::MaybeShowUpNextView,
base::Unretained(this)));
}
CalendarView::~CalendarView() {
is_destroying_ = true;
RestoreHeadersStatus();
RestoreMonthStatus();
// Removes child views including month views and event list to remove their
// dependency from `CalendarViewController`, since these views are destructed
// after the controller.
if (event_list_view_) {
calendar_sliding_surface_->RemoveChildViewT(event_list_view_.get());
event_list_view_ = nullptr;
}
check_upcoming_events_timer_.Stop();
RemoveUpNextView();
content_view_->RemoveAllChildViews();
}
void CalendarView::CreateExtraTitleRowButtons() {
tri_view()->SetContainerVisible(TriView::Container::END, /*visible=*/true);
if (calendar_utils::IsDisabledByAdmin()) {
DCHECK(!managed_button_);
managed_button_ = tri_view()->AddView(
TriView::Container::END,
std::make_unique<IconButton>(
base::BindRepeating(
&UnifiedSystemTrayController::HandleEnterpriseInfoAction,
base::Unretained(controller_)),
IconButton::Type::kMedium, &kSystemTrayManagedIcon,
IDS_ASH_CALENDAR_DISABLED_BY_ADMIN));
}
DCHECK(!reset_to_today_button_);
reset_to_today_button_ = CreateInfoButton(
base::BindRepeating(&CalendarView::ResetToTodayWithAnimation,
base::Unretained(this)),
IDS_ASH_CALENDAR_INFO_BUTTON_ACCESSIBLE_DESCRIPTION);
reset_to_today_button_->SetTooltipText(
l10n_util::GetStringUTF16(IDS_ASH_CALENDAR_TODAY_BUTTON_TOOLTIP));
tri_view()->AddView(TriView::Container::END, reset_to_today_button_);
DCHECK(!settings_button_);
settings_button_ = CreateSettingsButton(
base::BindRepeating(
&UnifiedSystemTrayController::HandleOpenDateTimeSettingsAction,
base::Unretained(controller_)),
IDS_ASH_CALENDAR_SETTINGS);
settings_button_->SetTooltipText(
l10n_util::GetStringUTF16(IDS_ASH_CALENDAR_SETTINGS_TOOLTIP));
tri_view()->AddView(TriView::Container::END, settings_button_);
}
views::Button* CalendarView::CreateInfoButton(
views::Button::PressedCallback callback,
int info_accessible_name_id) {
auto* button =
new PillButton(std::move(callback),
l10n_util::GetStringUTF16(IDS_ASH_CALENDAR_INFO_BUTTON),
PillButton::Type::kDefaultWithoutIcon, /*icon=*/nullptr);
button->SetAccessibleName(l10n_util::GetStringFUTF16(
IDS_ASH_CALENDAR_INFO_BUTTON_ACCESSIBLE_DESCRIPTION,
calendar_utils::GetMonthDayYear(base::Time::Now())));
return button;
}
void CalendarView::SetMonthViews() {
previous_label_ = AddLabelWithId(LabelType::PREVIOUS);
previous_month_ =
AddMonth(calendar_view_controller_->GetPreviousMonthFirstDayUTC(1));
current_label_ = AddLabelWithId(LabelType::CURRENT);
current_month_ =
AddMonth(calendar_view_controller_->GetOnScreenMonthFirstDayUTC());
next_label_ = AddLabelWithId(LabelType::NEXT);
next_month_ = AddMonth(calendar_view_controller_->GetNextMonthFirstDayUTC(1));
next_next_label_ = AddLabelWithId(LabelType::NEXTNEXT);
next_next_month_ = AddMonth(
calendar_view_controller_->GetNextMonthFirstDayUTC(/*num_months=*/2));
}
int CalendarView::PositionOfCurrentMonth() const {
// Compute the position, because this information may be required before
// layout.
return kContentVerticalPadding +
previous_label_->GetPreferredSize().height() +
previous_month_->GetPreferredSize().height() +
current_label_->GetPreferredSize().height();
}
int CalendarView::PositionOfToday() const {
return PositionOfCurrentMonth() +
calendar_view_controller_->GetTodayRowTopHeight();
}
int CalendarView::PositionOfSelectedDate() const {
DCHECK(calendar_view_controller_->selected_date().has_value());
const int row_height = calendar_view_controller_->selected_date_row_index() *
calendar_view_controller_->row_height() +
GetExpandedCalendarPadding();
// The selected date should be either in the current month or the next month.
if (calendar_view_controller_->IsSelectedDateInCurrentMonth()) {
return PositionOfCurrentMonth() + row_height;
}
return PositionOfCurrentMonth() +
current_month_->GetPreferredSize().height() +
next_label_->GetPreferredSize().height() + row_height;
}
void CalendarView::SetHeaderAndContentViewOpacity(float opacity) {
header_->layer()->SetOpacity(opacity);
content_view_->layer()->SetOpacity(opacity);
}
void CalendarView::SetShouldMonthsAnimateAndScrollEnabled(bool enabled) {
set_should_months_animate(enabled);
is_resetting_scroll_ = !enabled;
scroll_view_->SetVerticalScrollBarMode(
enabled ? views::ScrollView::ScrollBarMode::kHiddenButEnabled
: views::ScrollView::ScrollBarMode::kDisabled);
}
void CalendarView::ResetToTodayWithAnimation() {
if (!should_months_animate_) {
return;
}
SetShouldMonthsAnimateAndScrollEnabled(/*enabled=*/false);
auto content_reporter = calendar_metrics::CreateAnimationReporter(
content_view_, kContentViewResetToTodayAnimationHistogram);
auto header_reporter = calendar_metrics::CreateAnimationReporter(
header_, kHeaderViewResetToTodayAnimationHistogram);
// Fades out on-screen month. When animation ends sets date to today by
// calling `ResetToToday` and fades in updated views after.
views::AnimationBuilder()
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.OnEnded(base::BindOnce(&CalendarView::OnResetToTodayAnimationComplete,
weak_factory_.GetWeakPtr()))
.OnAborted(base::BindOnce(&CalendarView::OnResetToTodayAnimationComplete,
weak_factory_.GetWeakPtr()))
.Once()
.SetDuration(calendar_utils::kResetToTodayFadeAnimationDuration)
.SetOpacity(header_, 0.0f)
.SetOpacity(content_view_, 0.0f);
}
void CalendarView::ResetToToday() {
if (event_list_view_) {
scroll_view_->SetVerticalScrollBarMode(
views::ScrollView::ScrollBarMode::kHiddenButEnabled);
set_should_months_animate(false);
}
// Updates month to today's date without animating header.
{
base::AutoReset<bool> is_updating_month(&should_header_animate_, false);
calendar_view_controller_->UpdateMonth(base::Time::Now());
}
content_view_->RemoveChildViewT(previous_label_.get());
content_view_->RemoveChildViewT(previous_month_.get());
content_view_->RemoveChildViewT(current_label_.get());
content_view_->RemoveChildViewT(current_month_.get());
content_view_->RemoveChildViewT(next_label_.get());
content_view_->RemoveChildViewT(next_month_.get());
content_view_->RemoveChildViewT(next_next_label_.get());
content_view_->RemoveChildViewT(next_next_month_.get());
// Before adding new label and month views, reset the `scroll_view_` to 0
// position. Otherwise after all the views are deleted the 'scroll_view_`'s
// position is still pointing to the position of the previous `current_month_`
// view which is outside of the view. This will cause the scroll view show
// nothing on the screen and get stuck there (can not scroll up and down any
// more).
{
base::AutoReset<bool> is_resetting_scrolling(&is_resetting_scroll_, true);
scroll_view_->ScrollToPosition(scroll_view_->vertical_scroll_bar(), 0);
}
SetMonthViews();
ScrollToToday();
MaybeResetContentViewFocusBehavior();
if (event_list_view_) {
// `ShowEventListView()` also updates the selected view.
DCHECK(current_month_->has_today());
calendar_view_controller_->ShowEventListView(
calendar_view_controller_->todays_date_cell_view(), base::Time::Now(),
calendar_view_controller_->today_row());
months_animation_restart_timer_.Reset();
SetShouldMonthsAnimateAndScrollEnabled(/*enabled=*/true);
scroll_view_->SetVerticalScrollBarMode(
views::ScrollView::ScrollBarMode::kDisabled);
}
}
void CalendarView::UpdateOnScreenMonthMap() {
base::Time current_date = calendar_view_controller_->currently_shown_date();
base::Time start_time = calendar_utils::GetStartOfMonthUTC(
current_date + calendar_utils::GetTimeDifference(current_date));
on_screen_month_.clear();
on_screen_month_[start_time] =
calendar_model_->FindFetchingStatus(start_time);
// Checks if `next_month_` is in the visible view. If so, adds it to
// `on_screen_month_` if not already presents. Otherwise updates the fetching
// status. This is needed since a refetching request may be sent when this
// function is called and we need to update the fetching status to toggle the
// visibility of the loading bar.
if (scroll_view_->GetVisibleRect().bottom() >= next_month_->y()) {
base::Time next_start_time =
calendar_utils::GetStartOfNextMonthUTC(start_time);
on_screen_month_[next_start_time] =
calendar_model_->FindFetchingStatus(next_start_time);
// Checks if `next_next_month_` is in the visible view.
if (scroll_view_->GetVisibleRect().bottom() >= next_next_month_->y()) {
base::Time next_next_start_time =
calendar_utils::GetStartOfNextMonthUTC(next_start_time);
on_screen_month_[next_next_start_time] =
calendar_model_->FindFetchingStatus(next_next_start_time);
}
}
MaybeUpdateLoadingBarVisibility();
calendar_view_controller_->CalendarLoaded();
}
bool CalendarView::EventsFetchComplete() {
for (auto& it : on_screen_month_) {
// Return false if there's an on-screen month that hasn't finished fetching
// or re-fetching.
if (it.second == CalendarModel::kFetching ||
it.second == CalendarModel::kRefetching) {
return false;
}
}
return true;
}
void CalendarView::MaybeUpdateLoadingBarVisibility() {
ShowProgress(-1, !EventsFetchComplete());
}
void CalendarView::FadeInCurrentMonth() {
if (!should_months_animate_) {
return;
}
SetShouldMonthsAnimateAndScrollEnabled(/*enabled=*/false);
content_view_->SetPaintToLayer();
content_view_->layer()->SetFillsBoundsOpaquely(false);
SetHeaderAndContentViewOpacity(/*opacity=*/0.0f);
auto content_reporter = calendar_metrics::CreateAnimationReporter(
content_view_, kContentViewFadeInCurrentMonthAnimationHistogram);
auto header_reporter = calendar_metrics::CreateAnimationReporter(
header_, kHeaderViewFadeInCurrentMonthAnimationHistogram);
views::AnimationBuilder()
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.OnEnded(
base::BindOnce(&CalendarView::OnResetToTodayFadeInAnimationComplete,
weak_factory_.GetWeakPtr()))
.OnAborted(
base::BindOnce(&CalendarView::OnResetToTodayFadeInAnimationComplete,
weak_factory_.GetWeakPtr()))
.Once()
.SetDuration(calendar_utils::kResetToTodayFadeAnimationDuration)
.SetOpacity(header_, 1.0f)
.SetOpacity(content_view_, 1.0f);
}
void CalendarView::UpdateHeaders() {
header_->UpdateHeaders(
calendar_view_controller_->GetOnScreenMonthName(),
calendar_utils::GetYear(
calendar_view_controller_->currently_shown_date()));
}
void CalendarView::RestoreHeadersStatus() {
header_->layer()->GetAnimator()->StopAnimating();
header_->layer()->SetOpacity(1.0f);
header_->layer()->SetTransform(gfx::Transform());
temp_header_->layer()->GetAnimator()->StopAnimating();
temp_header_->SetVisible(false);
scrolling_settled_timer_.Reset();
if (!should_header_animate_) {
header_animation_restart_timer_.Reset();
}
}
void CalendarView::RestoreMonthStatus() {
current_month_->layer()->GetAnimator()->StopAnimating();
current_label_->layer()->GetAnimator()->StopAnimating();
previous_month_->layer()->GetAnimator()->StopAnimating();
previous_label_->layer()->GetAnimator()->StopAnimating();
next_label_->layer()->GetAnimator()->StopAnimating();
next_month_->layer()->GetAnimator()->StopAnimating();
next_next_label_->layer()->GetAnimator()->StopAnimating();
next_next_month_->layer()->GetAnimator()->StopAnimating();
ResetLayer(current_month_);
ResetLayer(current_label_);
ResetLayer(previous_label_);
ResetLayer(previous_month_);
ResetLayer(next_label_);
ResetLayer(next_month_);
ResetLayer(next_next_label_);
ResetLayer(next_next_month_);
if (!should_months_animate_) {
months_animation_restart_timer_.Reset();
}
}
void CalendarView::ScrollToToday() {
base::AutoReset<bool> is_resetting_scrolling(&is_resetting_scroll_, true);
if (event_list_view_ || up_next_view_) {
scroll_view_->ScrollToPosition(
scroll_view_->vertical_scroll_bar(),
PositionOfToday() + GetExpandedCalendarPadding());
return;
}
scroll_view_->ScrollToPosition(scroll_view_->vertical_scroll_bar(),
PositionOfCurrentMonth());
// If the screen does not have enough height which makes today's cell not in
// the visible rect, we auto scroll to today's row instead of scrolling to the
// first row of the current month.
if (PositionOfCurrentMonth() +
calendar_view_controller_->GetTodayRowBottomHeight() >
scroll_view_->GetVisibleRect().bottom()) {
scroll_view_->ScrollToPosition(scroll_view_->vertical_scroll_bar(),
PositionOfToday());
}
}
bool CalendarView::IsDateCellViewFocused() {
// For tests, in which the view is not in a Widget.
if (!GetFocusManager()) {
return false;
}
auto* focused_view = GetFocusManager()->GetFocusedView();
if (!focused_view) {
return false;
}
return focused_view->GetClassName() == CalendarDateCellView::kViewClassName;
}
bool CalendarView::IsAnimating() {
return header_->layer()->GetAnimator()->is_animating() ||
current_month_->layer()->GetAnimator()->is_animating() ||
content_view_->layer()->GetAnimator()->is_animating() ||
(event_list_view_ &&
event_list_view_->layer()->GetAnimator()->is_animating()) ||
calendar_sliding_surface_->layer()->GetAnimator()->is_animating();
}
void CalendarView::MaybeResetContentViewFocusBehavior() {
if (IsDateCellViewFocused() ||
content_view_->GetFocusBehavior() == FocusBehavior::ALWAYS) {
return;
}
content_view_->SetFocusBehavior(FocusBehavior::ALWAYS);
}
void CalendarView::OnViewBoundsChanged(views::View* observed_view) {
// When in the tablet mode and the display rotates and the `event_list_view_`
// is shown, `event_list_view_` should update its height to fill out the
// remaining space.
if (observed_view == this && event_list_view_) {
SetCalendarSlidingSurfaceBounds(true);
return;
}
// For screen density or orientation changes, we need to redraw the up next
// views position and adjust the scroll view height accordingly.
if (observed_view == this && up_next_view_) {
SetCalendarSlidingSurfaceBounds(false);
ClipScrollViewHeight(ScrollViewState::UP_NEXT_SHOWING);
return;
}
if (observed_view != scroll_view_) {
return;
}
// The CalendarView is created and lives without being added to the view tree
// for a while. The first time OnViewBoundsChanged is called is the sign that
// the view has actually been added to a view hierarchy, and it is time to
// make some changes which depend on the view belonging to a widget.
scoped_view_observer_.RemoveObservation(observed_view);
// Initializes the view to auto scroll to `PositionOfToday` or the first row
// of today's month.
ScrollToToday();
// If the view was shown via keyboard shortcut, the widget will be focusable.
// Request focus to enable the user to quickly press enter to see todays
// events. If the view was not shown via keyboard, this will be a no-op.
RequestFocus();
// Reset the timer here to invoke `UpdateOnScreenMonthMap()` manually after
// 'kScrollingSettledTimeout` since layout will be finalized after a few
// iterations and `on_screen_month_` only wants the final result.
scrolling_settled_timer_.Reset();
}
void CalendarView::OnViewFocused(View* observed_view) {
if (observed_view == this) {
content_view_->RequestFocus();
SetFocusBehavior(FocusBehavior::NEVER);
return;
}
if (observed_view != content_view_ || IsDateCellViewFocused()) {
return;
}
auto* focus_manager = GetFocusManager();
previous_month_->EnableFocus();
current_month_->EnableFocus();
next_month_->EnableFocus();
next_next_month_->EnableFocus();
// If the event list is showing, focus on the first cell in the current row or
// today's cell if today is in this row.
if (event_list_view_) {
focus_manager->SetFocusedView(
current_month_->focused_cells()[calendar_view_controller_
->GetExpandedRowIndex()]);
AdjustDateCellVoxBounds();
content_view_->SetFocusBehavior(FocusBehavior::NEVER);
return;
}
FocusPreferredDateCellViewOrFirstVisible(/*prefer_today=*/true);
}
views::View* CalendarView::AddLabelWithId(LabelType type, bool add_at_front) {
auto label = std::make_unique<MonthHeaderLabelView>(
type, calendar_view_controller_.get());
if (add_at_front) {
return content_view_->AddChildViewAt(std::move(label), 0);
}
return content_view_->AddChildView(std::move(label));
}
CalendarMonthView* CalendarView::AddMonth(base::Time month_first_date,
bool add_at_front) {
auto month = std::make_unique<CalendarMonthView>(
month_first_date, calendar_view_controller_.get());
month->SetBorder(views::CreateEmptyBorder(
gfx::Insets::TLBR(kMonthVerticalPadding, 0, kMonthVerticalPadding, 0)));
if (add_at_front) {
return content_view_->AddChildViewAt(std::move(month), 0);
} else {
return content_view_->AddChildView(std::move(month));
}
}
void CalendarView::OnMonthChanged() {
// The header animation without event list view is handled in the
// `ScrollOneMonthWithAnimation` method.
if (!should_header_animate_ || !event_list_view_) {
UpdateHeaders();
RestoreHeadersStatus();
return;
}
set_should_header_animate(false);
const std::u16string month =
calendar_view_controller_->GetOnScreenMonthName();
const std::u16string year = calendar_utils::GetYear(
calendar_view_controller_->currently_shown_date());
gfx::Transform header_moving = GetHeaderMovingAndPrepareAnimation(
is_scrolling_up_, kOnMonthChangedAnimationHistogram, month, year);
views::AnimationBuilder()
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.OnEnded(base::BindOnce(
[](base::WeakPtr<CalendarView> calendar_view) {
if (!calendar_view) {
return;
}
calendar_view->UpdateHeaders();
calendar_view->temp_header_->SetVisible(false);
calendar_view->header_->layer()->SetOpacity(1.0f);
calendar_view->header_->layer()->SetTransform(gfx::Transform());
calendar_view->set_should_header_animate(true);
calendar_view->reset_scrolling_settled_timer();
},
weak_factory_.GetWeakPtr()))
.OnAborted(base::BindOnce(
[](base::WeakPtr<CalendarView> calendar_view) {
if (!calendar_view) {
return;
}
calendar_view->temp_header_->SetVisible(false);
calendar_view->UpdateHeaders();
calendar_view->RestoreHeadersStatus();
},
weak_factory_.GetWeakPtr()))
.Once()
.SetDuration(calendar_utils::kAnimationDurationForMoving)
.SetTransform(header_, header_moving, gfx::Tween::EASE_OUT_2)
.SetTransform(temp_header_, gfx::Transform(), gfx::Tween::EASE_OUT_2)
.At(kDelayHeaderAnimationDuration)
.SetDuration(calendar_utils::kAnimationDurationForVisibility)
.SetOpacity(header_, 0.0f)
.At(kDelayHeaderAnimationDuration +
calendar_utils::kAnimationDurationForVisibility)
.SetDuration(calendar_utils::kAnimationDurationForVisibility)
.SetOpacity(temp_header_, 1.0f);
}
void CalendarView::OnEventsFetched(
const CalendarModel::FetchingStatus status,
const base::Time start_time,
const google_apis::calendar::EventList* events) {
if (on_screen_month_.find(start_time) != on_screen_month_.end()) {
on_screen_month_[start_time] = status;
}
MaybeUpdateLoadingBarVisibility();
// Only show up next for events that are the same month as `base::Time::Now`.
if (start_time == calendar_utils::GetStartOfMonthUTC(
base::Time::NowFromSystemTime().UTCMidnight())) {
MaybeShowUpNextView();
}
}
void CalendarView::OnTimeout(const base::Time start_time) {
if (on_screen_month_.find(start_time) != on_screen_month_.end()) {
on_screen_month_[start_time] = CalendarModel::kNever;
}
MaybeUpdateLoadingBarVisibility();
}
void CalendarView::OpenEventList() {
// Don't show the the `event_list_` view for unlogged in users.
if (!calendar_utils::ShouldFetchEvents()) {
return;
}
// If the event list is already open or if any animation is occurring do not
// let the user open the EventListView. It is ok to show the EventListView if
// the animation cooldown is active.
if (event_list_view_ || is_calendar_view_scrolling_ || IsAnimating()) {
return;
}
scroll_view_->SetVerticalScrollBarMode(
views::ScrollView::ScrollBarMode::kDisabled);
// Updates `scroll_view_`'s accessible name with the selected date.
absl::optional<base::Time> selected_date =
calendar_view_controller_->selected_date();
scroll_view_->GetViewAccessibility().OverrideName(l10n_util::GetStringFUTF16(
IDS_ASH_CALENDAR_CONTENT_ACCESSIBLE_DESCRIPTION,
calendar_utils::GetMonthNameAndYear(
calendar_view_controller_->currently_shown_date()),
calendar_utils::GetMonthDayYear(selected_date.value())));
scroll_view_->NotifyAccessibilityEvent(ax::mojom::Event::kTextChanged,
/*send_native_event=*/true);
event_list_view_ = calendar_sliding_surface_->AddChildView(
std::make_unique<CalendarEventListView>(calendar_view_controller_.get()));
event_list_view_->SetFocusBehavior(FocusBehavior::NEVER);
const int previous_surface_y = calendar_sliding_surface_->y();
SetCalendarSlidingSurfaceBounds(true);
set_should_months_animate(false);
gfx::Vector2dF moving_up_location = gfx::Vector2dF(
0, -PositionOfSelectedDate() + scroll_view_->GetVisibleRect().y());
gfx::Transform month_moving;
month_moving.Translate(moving_up_location);
// If the `up_next_view_` is showing, then we want to start the animation from
// there, otherwise we start from the bottom of the screen.
const int y_transform_start_position =
up_next_view_ ? previous_surface_y - calendar_sliding_surface_->y()
: calendar_sliding_surface_->y();
std::unique_ptr<ui::InterpolatedTranslation> list_view_sliding_up =
std::make_unique<ui::InterpolatedTranslation>(
gfx::PointF(0.f, y_transform_start_position), gfx::PointF());
// Tracks animation smoothness. For now, we only track animation smoothness
// for 1 month and 1 label since all 2 month views and 2 label views are
// similar and perform the same animation. If this is not the case in the
// future, we should add additional metrics for the rest.
auto month_reporter = calendar_metrics::CreateAnimationReporter(
current_month_, kMonthViewOpenEventListAnimationHistogram);
auto label_reporter = calendar_metrics::CreateAnimationReporter(
current_label_, kLabelViewOpenEventListAnimationHistogram);
auto event_list_reporter = calendar_metrics::CreateAnimationReporter(
event_list_view_, kEventListViewOpenEventListAnimationHistogram);
auto calendar_sliding_surface_reporter =
calendar_metrics::CreateAnimationReporter(
calendar_sliding_surface_,
kCalendarSlidingSurfaceOpenEventListAnimationHistogram);
views::AnimationBuilder()
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.OnEnded(base::BindOnce(&CalendarView::OnOpenEventListAnimationComplete,
weak_factory_.GetWeakPtr()))
.OnAborted(base::BindOnce(&CalendarView::OnOpenEventListAnimationComplete,
weak_factory_.GetWeakPtr()))
.Once()
.SetDuration(calendar_utils::kAnimationDurationForMoving)
.SetTransform(current_month_, month_moving, gfx::Tween::EASE_OUT_2)
.SetTransform(current_label_, month_moving, gfx::Tween::EASE_OUT_2)
.SetTransform(next_label_, month_moving, gfx::Tween::EASE_OUT_2)
.SetTransform(next_month_, month_moving, gfx::Tween::EASE_OUT_2)
.SetTransform(next_next_label_, month_moving, gfx::Tween::EASE_OUT_2)
.SetTransform(next_next_month_, month_moving, gfx::Tween::EASE_OUT_2)
.At(base::Milliseconds(0))
.SetDuration(kAnimationDurationForEventsMoving)
.SetInterpolatedTransform(calendar_sliding_surface_,
std::move(list_view_sliding_up),
gfx::Tween::EASE_IN_OUT_2);
if (up_next_view_) {
auto up_next_reporter = calendar_metrics::CreateAnimationReporter(
up_next_view_, kUpNextViewOpenEventListAnimationHistogram);
// Fade in `event_list_view_` and fade out `up_next_view_`.
views::AnimationBuilder()
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.Once()
.SetOpacity(event_list_view_, 0.f)
.SetOpacity(up_next_view_, 1.f)
.At(base::Milliseconds(0))
.SetDuration(kAnimationDurationForClosingEvents)
.SetOpacity(event_list_view_, 1.f)
.SetOpacity(up_next_view_, 0.f, gfx::Tween::EASE_IN);
}
}
void CalendarView::CloseEventList() {
// Don't allow the EventListView to close if an animation is
// occurring. It is ok to animate the EventListView if the animation cooldown
// is active.
if (IsAnimating()) {
return;
}
// Updates `scroll_view_`'s accessible name without the selected date.
scroll_view_->GetViewAccessibility().OverrideName(l10n_util::GetStringFUTF16(
IDS_ASH_CALENDAR_BUBBLE_ACCESSIBLE_DESCRIPTION,
calendar_utils::GetMonthDayYearWeek(
calendar_view_controller_->currently_shown_date())));
scroll_view_->NotifyAccessibilityEvent(ax::mojom::Event::kTextChanged,
/*send_native_event=*/true);
// Increase the scroll height before the animation starts, so that it's
// already full height when animating the event list view sliding down.
ClipScrollViewHeight(up_next_view_ ? ScrollViewState::UP_NEXT_SHOWING
: ScrollViewState::FULL_HEIGHT);
scroll_view_->SetVerticalScrollBarMode(
views::ScrollView::ScrollBarMode::kHiddenButEnabled);
// Move EventListView to the top of the up next view if showing, or off the
// bottom of the CalendarView.
const int previous_surface_y = calendar_sliding_surface_->y();
SetCalendarSlidingSurfaceBounds(false);
std::unique_ptr<ui::InterpolatedTranslation> list_view_sliding_down =
std::make_unique<ui::InterpolatedTranslation>(
gfx::PointF(0.f, previous_surface_y - calendar_sliding_surface_->y()),
gfx::PointF());
auto event_list_reporter = calendar_metrics::CreateAnimationReporter(
event_list_view_, kCloseEventListAnimationHistogram);
auto calendar_sliding_surface_reporter =
calendar_metrics::CreateAnimationReporter(
calendar_sliding_surface_,
kCloseEventListCalendarSlidingSurfaceAnimationHistogram);
views::AnimationBuilder()
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.OnEnded(base::BindOnce(&CalendarView::OnCloseEventListAnimationComplete,
weak_factory_.GetWeakPtr()))
.OnAborted(
base::BindOnce(&CalendarView::OnCloseEventListAnimationComplete,
weak_factory_.GetWeakPtr()))
.Once()
.SetDuration(kAnimationDurationForClosingEvents)
.SetInterpolatedTransform(calendar_sliding_surface_,
std::move(list_view_sliding_down),
gfx::Tween::FAST_OUT_SLOW_IN)
// Fade out the event list view.
.At(kEventListAnimationStartDelay)
.SetDuration(kAnimationDurationForClosingEvents)
.SetOpacity(event_list_view_, 0.f, gfx::Tween::FAST_OUT_SLOW_IN);
// Fade in the up next view.
if (up_next_view_) {
if (!up_next_view_->GetVisible()) {
up_next_view_->SetVisible(true);
}
auto up_next_reporter = calendar_metrics::CreateAnimationReporter(
up_next_view_, kCloseEventListUpNextViewAnimationHistogram);
views::AnimationBuilder()
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.Once()
.SetOpacity(up_next_view_, 0.f)
.At(kUpNextAnimationStartDelay)
.SetDuration(kAnimationDurationForClosingEvents)
.SetOpacity(up_next_view_, 1.f, gfx::Tween::EASE_OUT);
}
}
void CalendarView::OnSelectedDateUpdated() {
// If the event list is already open and the date cell is focused, moves the
// focusing ring to the close button.
if (event_list_view_ && IsDateCellViewFocused()) {
RequestFocusForEventListCloseButton();
}
}
void CalendarView::OnCalendarLoaded() {
// We might have some cached upcoming events so we can show the
// `up_next_view_` as soon as the calendar has loaded i.e. before waiting for
// the event fetch to complete.
MaybeShowUpNextView();
}
void CalendarView::ScrollUpOneMonth() {
calendar_view_controller_->UpdateMonth(
calendar_view_controller_->GetPreviousMonthFirstDayUTC(1));
content_view_->RemoveChildViewT(next_next_label_.get());
content_view_->RemoveChildViewT(next_next_month_.get());
next_next_label_ = next_label_;
next_next_month_ = next_month_;
next_label_ = current_label_;
next_month_ = current_month_;
current_label_ = previous_label_;
current_month_ = previous_month_;
previous_month_ =
AddMonth(calendar_view_controller_->GetPreviousMonthFirstDayUTC(1),
/*add_at_front=*/true);
if (IsDateCellViewFocused()) {
previous_month_->EnableFocus();
}
previous_label_ = AddLabelWithId(LabelType::PREVIOUS,
/*add_at_front=*/true);
// After adding a new month in the content, the current position stays the
// same but below the added view the each view's position has changed to
// [original position + new month's height]. So we need to add the height of
// the newly added month to keep the current view's position.
int added_height = previous_month_->GetPreferredSize().height() +
previous_label_->GetPreferredSize().height();
int position = added_height + scroll_view_->GetVisibleRect().y();
base::AutoReset<bool> is_resetting_scrolling(&is_resetting_scroll_, true);
scroll_view_->ScrollToPosition(scroll_view_->vertical_scroll_bar(), position);
MaybeResetContentViewFocusBehavior();
if (current_month_->has_events()) {
calendar_view_controller_->EventsDisplayedToUser();
}
}
void CalendarView::ScrollDownOneMonth() {
// Renders the next month if the next month label is moving up and passing
// the top of the visible area, or the next month body's bottom is passing
// the bottom of the visible area.
int removed_height = previous_month_->GetPreferredSize().height() +
previous_label_->GetPreferredSize().height();
calendar_view_controller_->UpdateMonth(
calendar_view_controller_->GetNextMonthFirstDayUTC(1));
content_view_->RemoveChildViewT(previous_label_.get());
content_view_->RemoveChildViewT(previous_month_.get());
previous_label_ = current_label_;
previous_month_ = current_month_;
current_label_ = next_label_;
current_month_ = next_month_;
next_label_ = next_next_label_;
next_month_ = next_next_month_;
next_next_label_ = AddLabelWithId(LabelType::NEXTNEXT);
next_next_month_ = AddMonth(
calendar_view_controller_->GetNextMonthFirstDayUTC(/*num_months=*/2));
if (IsDateCellViewFocused()) {
next_next_month_->EnableFocus();
}
// Same as adding previous views. We need to remove the height of the
// deleted month to keep the current view's position.
int position = scroll_view_->GetVisibleRect().y() - removed_height;
base::AutoReset<bool> is_resetting_scrolling(&is_resetting_scroll_, true);
scroll_view_->ScrollToPosition(scroll_view_->vertical_scroll_bar(), position);
MaybeResetContentViewFocusBehavior();
if (current_month_->has_events()) {
calendar_view_controller_->EventsDisplayedToUser();
}
}
void CalendarView::ScrollOneMonthAndAutoScroll(bool scroll_up) {
if (is_resetting_scroll_) {
return;
}
base::AutoReset<bool> is_resetting_scrolling(&is_resetting_scroll_, true);
RestoreMonthStatus();
if (scroll_up) {
ScrollUpOneMonth();
} else {
ScrollDownOneMonth();
}
scroll_view_->ScrollToPosition(scroll_view_->vertical_scroll_bar(),
PositionOfCurrentMonth());
}
void CalendarView::ScrollOneMonthWithAnimation(bool scroll_up) {
user_has_scrolled_ = true;
is_scrolling_up_ = scroll_up;
if (event_list_view_) {
// If it is animating to open this `event_list_view_`, disable the up/down
// buttons.
if (!should_months_animate_ || !should_header_animate_) {
return;
}
ScrollOneRowWithAnimation(scroll_up);
return;
}
// If there's already an existing animation, restores each layer's visibility
// and position.
if (!should_months_animate_ || !should_header_animate_) {
set_should_months_animate(false);
set_should_header_animate(false);
RestoreHeadersStatus();
is_resetting_scroll_ = false;
scroll_view_->SetVerticalScrollBarMode(
views::ScrollView::ScrollBarMode::kHiddenButEnabled);
ScrollOneMonthAndAutoScroll(scroll_up);
return;
}
if (is_resetting_scroll_) {
return;
}
// Starts to show the month and header animation.
SetShouldMonthsAnimateAndScrollEnabled(false);
set_should_header_animate(false);
gfx::Vector2dF moving_up_location = gfx::Vector2dF(
0, previous_month_->GetPreferredSize().height() +
current_label_->GetPreferredSize().height() +
(scroll_view_->GetVisibleRect().y() - current_month_->y()));
gfx::Vector2dF moving_down_location = gfx::Vector2dF(
0, -current_month_->GetPreferredSize().height() -
next_label_->GetPreferredSize().height() +
(scroll_view_->GetVisibleRect().y() - current_month_->y()));
gfx::Transform month_moving;
month_moving.Translate(scroll_up ? moving_up_location : moving_down_location);
const std::u16string temp_month =
scroll_up ? calendar_view_controller_->GetPreviousMonthName()
: calendar_view_controller_->GetNextMonthName();
const std::u16string temp_year = calendar_utils::GetYear(
scroll_up ? calendar_view_controller_->GetPreviousMonthFirstDayUTC(
/*num_months=*/1)
: calendar_view_controller_->GetNextMonthFirstDayUTC(
/*num_months=*/1));
gfx::Transform header_moving = GetHeaderMovingAndPrepareAnimation(
scroll_up, kHeaderViewScrollOneMonthAnimationHistogram, temp_month,
temp_year);
// Tracks animation smoothness. For now, we only track animation smoothness
// for 1 month and 1 label since all 3 month views and 3 label views are
// similar and perform the same animation. If this is not the case in the
// future, we should add additional metrics for the rest.
auto month_reporter = calendar_metrics::CreateAnimationReporter(
current_month_, kMonthViewScrollOneMonthAnimationHistogram);
auto label_reporter = calendar_metrics::CreateAnimationReporter(
current_label_, kLabelViewScrollOneMonthAnimationHistogram);
views::AnimationBuilder()
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.OnEnded(base::BindOnce(&CalendarView::OnScrollMonthAnimationComplete,
weak_factory_.GetWeakPtr(), scroll_up))
.OnAborted(base::BindOnce(&CalendarView::OnScrollMonthAnimationComplete,
weak_factory_.GetWeakPtr(), scroll_up))
.Once()
.SetDuration(calendar_utils::kAnimationDurationForMonthMoving)
.SetTransform(current_month_, month_moving, gfx::Tween::EASE_OUT_2)
.SetTransform(current_label_, month_moving, gfx::Tween::EASE_OUT_2)
.SetTransform(previous_month_, month_moving, gfx::Tween::EASE_OUT_2)
.SetTransform(previous_label_, month_moving, gfx::Tween::EASE_OUT_2)
.SetTransform(next_month_, month_moving, gfx::Tween::EASE_OUT_2)
.SetTransform(next_label_, month_moving, gfx::Tween::EASE_OUT_2)
.SetTransform(next_next_month_, month_moving, gfx::Tween::EASE_OUT_2)
.SetTransform(next_next_label_, month_moving, gfx::Tween::EASE_OUT_2)
.At(kDelayHeaderAnimationDuration)
.SetDuration(calendar_utils::kAnimationDurationForMoving)
.SetTransform(header_, header_moving, gfx::Tween::EASE_OUT_2)
.SetTransform(temp_header_, gfx::Transform(), gfx::Tween::EASE_OUT_2)
.At(kDelayHeaderAnimationDuration)
.SetDuration(calendar_utils::kAnimationDurationForVisibility)
.SetOpacity(header_, 0.0f)
.At(kDelayHeaderAnimationDuration +
calendar_utils::kAnimationDurationForVisibility)
.SetDuration(calendar_utils::kAnimationDurationForVisibility)
.SetOpacity(temp_header_, 1.0f);
}
gfx::Transform CalendarView::GetHeaderMovingAndPrepareAnimation(
bool scroll_up,
const std::string& animation_name,
const std::u16string& temp_month,
const std::u16string& temp_year) {
const int header_height = header_->GetPreferredSize().height();
const gfx::Vector2dF header_moving_location =
gfx::Vector2dF(0, (header_height / 2) * (scroll_up ? 1 : -1));
gfx::Transform header_moving;
header_moving.Translate(header_moving_location);
// Tracks animation smoothness.
auto header_reporter =
calendar_metrics::CreateAnimationReporter(header_, animation_name);
// Update the temp header label with the new header's month and year.
temp_header_->UpdateHeaders(temp_month, temp_year);
temp_header_->layer()->SetOpacity(0.0f);
gfx::Transform initial_state;
initial_state.Translate(
gfx::Vector2dF(0, (header_height / 2) * (scroll_up ? -1 : 1)));
temp_header_->layer()->SetTransform(initial_state);
temp_header_->SetVisible(true);
return header_moving;
}
void CalendarView::ScrollOneRowWithAnimation(bool scroll_up) {
if (is_resetting_scroll_) {
return;
}
is_scrolling_up_ = scroll_up;
scroll_view_->SetVerticalScrollBarMode(
views::ScrollView::ScrollBarMode::kHiddenButEnabled);
base::AutoReset<bool> is_resetting_scrolling(&is_resetting_scroll_, true);
// Scrolls to the last row of the previous month if it's currently on the
// first row and scrolling up.
if (scroll_up && calendar_view_controller_->GetExpandedRowIndex() == 0) {
ScrollUpOneMonth();
SetExpandedRowThenDisableScroll(current_month_->last_row_index());
return;
}
// Scrolls to the first row of the next month if it's currently on the
// last row and scrolling down.
if (!scroll_up && calendar_view_controller_->GetExpandedRowIndex() ==
current_month_->last_row_index()) {
ScrollDownOneMonth();
SetExpandedRowThenDisableScroll(0);
return;
}
SetExpandedRowThenDisableScroll(
calendar_view_controller_->GetExpandedRowIndex() + (scroll_up ? -1 : 1));
return;
}
void CalendarView::OnEvent(ui::Event* event) {
if (!event->IsKeyEvent()) {
TrayDetailedView::OnEvent(event);
return;
}
auto* key_event = event->AsKeyEvent();
auto key_code = key_event->key_code();
auto* focus_manager = GetFocusManager();
bool is_tab_key_pressed =
key_event->type() == ui::EventType::ET_KEY_PRESSED &&
views::FocusManager::IsTabTraversalKeyEvent(*key_event);
if (is_tab_key_pressed) {
RecordCalendarKeyboardNavigation(
calendar_metrics::CalendarKeyboardNavigationSource::kTab);
}
if (!IsDateCellViewFocused()) {
if (is_tab_key_pressed && key_event->IsShiftDown()) {
// If this is reverse tab navigation (Shift+Tab) and current focused view
// is the last focusable view, then make an attempt to navigate to the
// previous widget (most likely to the message center). Stop the
// propagation of the event if the attempt was successful.
const auto* next_reverse_view = focus_manager->GetNextFocusableView(
focus_manager->GetFocusedView(), GetWidget(), /*reverse=*/true,
/*dont_loop=*/true);
if (!next_reverse_view && controller_->FocusOut(/*reverse=*/true)) {
event->StopPropagation();
}
}
TrayDetailedView::OnEvent(event);
return;
}
// When tab key is pressed, stops focusing on any `CalendarDateCellView` and
// goes to the next focusable button in the header.
if (is_tab_key_pressed) {
// Set focus on `down_button_`/`event_list_view_` or null
// pointer to escape the focusing on the date cell.
if (key_event->IsShiftDown()) {
down_button_->RequestFocus();
} else if (event_list_view_) {
// Moves focusing ring to the close button of the event list.
event_list_view_->RequestFocus();
focus_manager->AdvanceFocus(/*reverse=*/false);
} else {
scroll_view_->SetFocusBehavior(FocusBehavior::ALWAYS);
scroll_view_->RequestFocus();
}
current_month_->DisableFocus();
previous_month_->DisableFocus();
next_month_->DisableFocus();
next_next_month_->DisableFocus();
TrayDetailedView::OnEvent(event);
// Should move the focus to the next widget, so `AdvanceFocus` from the last
// view.
if (!key_event->IsShiftDown() && !event_list_view_) {
focus_manager->AdvanceFocus(/*reverse=*/false);
scroll_view_->SetFocusBehavior(FocusBehavior::NEVER);
}
event->StopPropagation();
content_view_->SetFocusBehavior(FocusBehavior::ALWAYS);
return;
}
if (key_event->type() != ui::EventType::ET_KEY_PRESSED ||
(key_code != ui::VKEY_UP && key_code != ui::VKEY_DOWN &&
key_code != ui::VKEY_LEFT && key_code != ui::VKEY_RIGHT)) {
TrayDetailedView::OnEvent(event);
return;
}
switch (key_code) {
case ui::VKEY_UP:
case ui::VKEY_DOWN: {
RecordCalendarKeyboardNavigation(
calendar_metrics::CalendarKeyboardNavigationSource::kArrowKeys);
auto* current_focusable_view = focus_manager->GetFocusedView();
// Enable the scroll bar mode, in case it is disabled when the event list
// is showing.
scroll_view_->SetVerticalScrollBarMode(
views::ScrollView::ScrollBarMode::kHiddenButEnabled);
// Moving 7 (`kDateInOneWeek`) steps will focus on the cell which is right
// above or below the current cell, since each row has 7 days.
for (int count = 0; count < calendar_utils::kDateInOneWeek; count++) {
auto* next_focusable_view = focus_manager->GetNextFocusableView(
current_focusable_view, GetWidget(),
/*reverse=*/key_code == ui::VKEY_UP,
/*dont_loop=*/false);
current_focusable_view = next_focusable_view;
// Sometimes the position of the upper row cells, which should be
// focused next, are above (and hidden behind) the header buttons. So
// this loop skips those buttons.
while (current_focusable_view &&
current_focusable_view->GetClassName() !=
CalendarDateCellView::kViewClassName) {
current_focusable_view = focus_manager->GetNextFocusableView(
current_focusable_view, GetWidget(),
/*reverse=*/key_code == ui::VKEY_UP,
/*dont_loop=*/false);
}
}
focus_manager->SetFocusedView(current_focusable_view);
// After focusing on the new cell the view should have scrolled already
// if needed, but there's an offset compared with scrolled by
// `ScrollOneRowWithAnimation`. Manually scroll the view then disable the
// scroll bar mode if the even list is showing.
if (event_list_view_) {
const int current_height =
scroll_view_->GetVisibleRect().y() - PositionOfCurrentMonth();
SetExpandedRowThenDisableScroll(
current_height / calendar_view_controller_->row_height());
}
AdjustDateCellVoxBounds();
return;
}
case ui::VKEY_LEFT:
case ui::VKEY_RIGHT: {
RecordCalendarKeyboardNavigation(
calendar_metrics::CalendarKeyboardNavigationSource::kArrowKeys);
// Enable the scroll bar mode, in case it is disabled when the event list
// is showing.
scroll_view_->SetVerticalScrollBarMode(
views::ScrollView::ScrollBarMode::kHiddenButEnabled);
bool is_reverse = base::i18n::IsRTL() ? key_code == ui::VKEY_RIGHT
: key_code == ui::VKEY_LEFT;
focus_manager->AdvanceFocus(/*reverse=*/is_reverse);
// After focusing on the new cell the view should have scrolled already
// if needed, but there's an offset compared with scrolled by
// `ScrollOneRowWithAnimation`. Manually scroll the view then disable the
// scroll bar mode if the even list is showing.
if (event_list_view_) {
const int current_height =
scroll_view_->GetVisibleRect().y() - PositionOfCurrentMonth();
SetExpandedRowThenDisableScroll(
current_height / calendar_view_controller_->row_height());
}
AdjustDateCellVoxBounds();
return;
}
default:
NOTREACHED();
}
}
void CalendarView::SetExpandedRowThenDisableScroll(int row_index) {
DCHECK(event_list_view_);
calendar_view_controller_->set_expanded_row_index(row_index);
const int row_height = calendar_view_controller_->GetExpandedRowIndex() *
calendar_view_controller_->row_height();
scroll_view_->ScrollToPosition(
scroll_view_->vertical_scroll_bar(),
PositionOfCurrentMonth() + row_height + GetExpandedCalendarPadding());
scroll_view_->SetVerticalScrollBarMode(
views::ScrollView::ScrollBarMode::kDisabled);
}
void CalendarView::OnContentsScrolled() {
base::AutoReset<bool> set_is_scrolling(&is_calendar_view_scrolling_, true);
// The scroll position is reset because it's adjusting the position when
// adding or removing views from the `scroll_view_`. It should scroll to the
// position we want, so we don't need to check the visible area position.
if (is_resetting_scroll_) {
return;
}
user_has_scrolled_ = true;
base::AutoReset<bool> disable_header_animation(&should_header_animate_,
false);
// Reset the timer to update the `on_screen_month_` map after scrolling.
scrolling_settled_timer_.Reset();
// Scrolls to the previous month if the current label is moving down and
// passing the top of the visible area.
if (scroll_view_->GetVisibleRect().y() <= current_label_->y()) {
ScrollUpOneMonth();
} else if (scroll_view_->GetVisibleRect().y() >= next_label_->y()) {
ScrollDownOneMonth();
}
}
void CalendarView::OnMonthArrowButtonActivated(bool up,
const ui::Event& event) {
calendar_metrics::RecordMonthArrowButtonActivated(up, event);
ScrollOneMonthWithAnimation(up);
content_view_->OnMonthChanged();
}
void CalendarView::AdjustDateCellVoxBounds() {
auto* focused_view = GetFocusManager()->GetFocusedView();
DCHECK_EQ(focused_view->GetClassName(), CalendarDateCellView::kViewClassName);
// When the Chrome Vox focusing box is in a `ScrollView`, the hidden content
// height, which is `scroll_view_->GetVisibleRect().y()` should also be added.
// Otherwise the position of the Chrome Vox box is off.
gfx::Rect bounds = focused_view->GetBoundsInScreen();
focused_view->GetViewAccessibility().OverrideBounds(
gfx::RectF(bounds.x(), bounds.y() + scroll_view_->GetVisibleRect().y(),
bounds.width(), bounds.height()));
}
void CalendarView::OnScrollMonthAnimationComplete(bool scroll_up) {
set_should_header_animate(true);
SetShouldMonthsAnimateAndScrollEnabled(true);
ScrollOneMonthAndAutoScroll(scroll_up);
temp_header_->SetVisible(false);
header_->layer()->SetOpacity(1.0f);
header_->layer()->SetTransform(gfx::Transform());
}
void CalendarView::OnOpenEventListAnimationComplete() {
if (is_destroying_) {
return;
}
scroll_view_->SetVerticalScrollBarMode(
views::ScrollView::ScrollBarMode::kHiddenButEnabled);
// Scrolls to the next month if the selected date is in the `next_month_`, so
// that the `current_month_`is updated to the next month.
if (!calendar_view_controller_->IsSelectedDateInCurrentMonth()) {
ScrollDownOneMonth();
}
// If still not in this month, it's in the `next_next_month_`. Doing this in a
// while loop may cause a potential infinite loop. For example when the time
// difference is not calculated or applied correctly, which may cause some
// dates cannot be found in the months.
if (!calendar_view_controller_->IsSelectedDateInCurrentMonth()) {
ScrollDownOneMonth();
}
base::AutoReset<bool> is_resetting_scrolling(&is_resetting_scroll_, true);
RestoreMonthStatus();
scroll_view_->ScrollToPosition(scroll_view_->vertical_scroll_bar(),
PositionOfSelectedDate());
// Clip the height to a bit more than the height of a row.
ClipScrollViewHeight(ScrollViewState::EVENT_LIST_SHOWING);
if (up_next_view_) {
// Once the animation is complete, the `up_next_view_` needs to be invisible
// otherwise ChromeVox will pick it up.
up_next_view_->SetVisible(false);
}
if (!should_months_animate_) {
months_animation_restart_timer_.Reset();
}
scroll_view_->SetVerticalScrollBarMode(
views::ScrollView::ScrollBarMode::kDisabled);
calendar_view_controller_->OnEventListOpened();
// Moves focusing ring to the close button of the event list if it's opened
// from the date cell view focus or from the `up_next_view_`.
if (IsDateCellViewFocused() || up_next_view_) {
RequestFocusForEventListCloseButton();
}
up_button_->SetTooltipText(l10n_util::GetStringUTF16(
IDS_ASH_CALENDAR_UP_BUTTON_EVENT_LIST_ACCESSIBLE_DESCRIPTION));
down_button_->SetTooltipText(l10n_util::GetStringUTF16(
IDS_ASH_CALENDAR_DOWN_BUTTON_EVENT_LIST_ACCESSIBLE_DESCRIPTION));
}
void CalendarView::OnCloseEventListAnimationComplete() {
if (is_destroying_) {
return;
}
// GetFocusManager() can be nullptr if `CalendarView` is destroyed when the
// closing animation hasn't finished.
auto* focused_view =
GetFocusManager() ? GetFocusManager()->GetFocusedView() : nullptr;
// Restore focus before removing `event_list_view_`. This is necessary because
// showing `event_list_view_` scrolls the `scroll_view_` with custom padding
// which is hard to detect after the fact. If `event_list_view_` doesn't
// exist, it's not clear the padding exists, and this can result in the wrong
// CalendarDateCellView being focused.
if (focused_view && Contains(focused_view)) {
FocusPreferredDateCellViewOrFirstVisible(/*prefer_today=*/false);
}
calendar_sliding_surface_->RemoveChildViewT(event_list_view_.get());
event_list_view_ = nullptr;
calendar_view_controller_->OnEventListClosed();
MaybeShowUpNextView();
up_button_->SetTooltipText(l10n_util::GetStringUTF16(
IDS_ASH_CALENDAR_UP_BUTTON_ACCESSIBLE_DESCRIPTION));
down_button_->SetTooltipText(l10n_util::GetStringUTF16(
IDS_ASH_CALENDAR_DOWN_BUTTON_ACCESSIBLE_DESCRIPTION));
}
void CalendarView::RequestFocusForEventListCloseButton() {
DCHECK(event_list_view_);
auto* focus_manager = GetFocusManager();
event_list_view_->RequestFocus();
focus_manager->AdvanceFocus(/*reverse=*/false);
current_month_->DisableFocus();
previous_month_->DisableFocus();
next_month_->DisableFocus();
next_next_month_->DisableFocus();
content_view_->SetFocusBehavior(FocusBehavior::ALWAYS);
}
void CalendarView::OnResetToTodayAnimationComplete() {
SetShouldMonthsAnimateAndScrollEnabled(/*enabled=*/true);
ResetToToday();
FadeInCurrentMonth();
// There's a corner case when the `current_month_` doesn't change,
// the `on_screen_month_` map won't be updated since
// `OnMonthChanged` won't be called and the timer won't be reset. So
// we manually call the timer to update `on_screen_month_`.
reset_scrolling_settled_timer();
}
void CalendarView::OnResetToTodayFadeInAnimationComplete() {
set_should_months_animate(true);
set_should_header_animate(true);
is_resetting_scroll_ = false;
scroll_view_->SetVerticalScrollBarMode(
event_list_view_ ? views::ScrollView::ScrollBarMode::kDisabled
: views::ScrollView::ScrollBarMode::kHiddenButEnabled);
SetHeaderAndContentViewOpacity(/*opacity=*/1.0f);
}
void CalendarView::FocusPreferredDateCellViewOrFirstVisible(bool prefer_today) {
previous_month_->EnableFocus();
current_month_->EnableFocus();
next_month_->EnableFocus();
next_next_month_->EnableFocus();
CalendarDateCellView* to_be_focused_cell =
GetTargetDateCellViewOrFirstFocusable(
prefer_today ? calendar_view_controller_->todays_date_cell_view()
: calendar_view_controller_->selected_date_cell_view());
if (to_be_focused_cell) {
to_be_focused_cell->SetFirstOnFocusedAccessibilityLabel();
GetFocusManager()->SetFocusedView(to_be_focused_cell);
} else {
// If there's no visible row of the current month on the screen, focus on
// the first visible non-grayed-out date of the next month.
GetFocusManager()->SetFocusedView(next_month_->focused_cells().front());
}
AdjustDateCellVoxBounds();
content_view_->SetFocusBehavior(FocusBehavior::NEVER);
}
CalendarDateCellView* CalendarView::GetTargetDateCellViewOrFirstFocusable(
CalendarDateCellView* target_date_cell_view) {
// When focusing on the `content_view_`, we decide which is the to-be-focused
// cell based on the current position.
const int visible_window_y_in_content_view =
scroll_view_->GetVisibleRect().y();
const int row_height = calendar_view_controller_->row_height();
// Check whether at least one row of the current month is visible on the
// screen. The to-be-focused cell should be the first non-grayed date cell
// that is visible, or today's cell if today is in the current month and
// visible.
if (visible_window_y_in_content_view >=
(next_label_->y() - row_height - kMonthVerticalPadding -
kLabelVerticalPadding)) {
return nullptr;
}
const int first_visible_row = CalculateFirstFullyVisibleRow();
if (target_date_cell_view &&
(current_month_ == target_date_cell_view->parent()) &&
(first_visible_row <= target_date_cell_view->row_index())) {
return target_date_cell_view;
}
return current_month_->focused_cells()[first_visible_row];
}
int CalendarView::CalculateFirstFullyVisibleRow() {
const int visible_window_y_in_content_view =
scroll_view_->GetVisibleRect().y();
int row_index = 0;
// Get first visible row index. If `event_list_view_` is showing, account
// for the extra padding added to `scroll_view_`'s visible window.
while (visible_window_y_in_content_view >
(PositionOfCurrentMonth() +
row_index * calendar_view_controller_->row_height() +
(event_list_view_ ? GetExpandedCalendarPadding() : 0))) {
++row_index;
if (row_index > kMaxRowsInOneMonth) {
NOTREACHED() << "CalendarMonthView's cannot have more than "
<< kMaxRowsInOneMonth << " rows.";
return kMaxRowsInOneMonth;
}
}
return row_index;
}
void CalendarView::SetCalendarSlidingSurfaceBounds(bool event_list_view_open) {
const int x_position = scroll_view_->x() + kEventListViewHorizontalOffset;
const int width = scroll_view_->GetVisibleRect().width() -
kEventListViewHorizontalOffset * 2;
const int event_list_view_height = GetBoundsInScreen().bottom() -
scroll_view_->GetBoundsInScreen().y() -
GetSingleVisibleRowHeight();
// If the event list view is showing, position the calendar sliding surface
// where the opened event list view will be.
if (event_list_view_open) {
calendar_sliding_surface_->SetBounds(
x_position, scroll_view_->y() + GetSingleVisibleRowHeight(), width,
event_list_view_height);
return;
}
// If the event list view is not showing and the up next view is showing,
// position the calendar sliding surface where the up next view will be.
if (up_next_view_) {
const int up_next_view_preferred_height =
up_next_view_->GetPreferredSize().height();
calendar_sliding_surface_->SetBounds(
x_position, GetVisibleBounds().bottom() - up_next_view_preferred_height,
width, event_list_view_height);
return;
}
// If neither event list nor up next are showing, position the calendar
// sliding surface off the bottom of the screen.
calendar_sliding_surface_->SetBounds(x_position, GetVisibleBounds().bottom(),
width, event_list_view_height);
}
void CalendarView::MaybeShowUpNextView() {
if (!features::IsCalendarJellyEnabled() ||
calendar_view_controller_->UpcomingEvents().empty()) {
RemoveUpNextView();
return;
}
if (up_next_view_) {
up_next_view_->RefreshEvents();
return;
}
// If the `event_list_view_` is currently showing and the `up_next_view_` is
// not, then early return. In this scenario we want the up next view to be
// there when the user closes the `event_list_view_` but we don't want all the
// scrollview clipping or bounds changing to happen.
if (event_list_view_) {
return;
}
up_next_view_ = calendar_sliding_surface_->AddChildView(
std::make_unique<CalendarUpNextView>(
calendar_view_controller_.get(),
base::BindRepeating(&CalendarView::OpenEventListForTodaysDate,
base::Unretained(this))));
// If the event list and up next views aren't currently displayed, then
// construct the view and put the calendar into the state of showing the up
// next view.
ClipScrollViewHeight(ScrollViewState::UP_NEXT_SHOWING);
SetCalendarSlidingSurfaceBounds(false);
// Translate the up next view `kUpNextAnimationYOffset` off the screen and
// animate sliding up.
std::unique_ptr<ui::InterpolatedTranslation> up_next_sliding_up =
std::make_unique<ui::InterpolatedTranslation>(
gfx::PointF(0.f, kUpNextAnimationYOffset), gfx::PointF());
auto up_next_view_reporter = calendar_metrics::CreateAnimationReporter(
up_next_view_, kShowUpNextViewAnimationHistogram);
// Animate the `up_next_view_` in.
views::AnimationBuilder()
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.OnAborted(base::BindOnce(&CalendarView::OnShowUpNextAnimationEnded,
weak_factory_.GetWeakPtr()))
.OnEnded(base::BindOnce(&CalendarView::OnShowUpNextAnimationEnded,
weak_factory_.GetWeakPtr()))
.Once()
.SetOpacity(up_next_view_, 0.f)
.At(base::Milliseconds(0))
.SetDuration(kAnimationDurationForClosingEvents)
.SetOpacity(up_next_view_, 1.f)
.SetInterpolatedTransform(calendar_sliding_surface_,
std::move(up_next_sliding_up),
gfx::Tween::FAST_OUT_SLOW_IN_2);
}
void CalendarView::OnShowUpNextAnimationEnded() {
// If todays date cell is null or the user has scrolled at all, then don't
// auto scroll.
if (!calendar_view_controller_->todays_date_cell_view() ||
user_has_scrolled_) {
return;
}
// If todays date cell is not visible in the `scroll_view_`, i.e. it's hidden
// behind the up next view, then smooth scroll to it.
if (!scroll_view_->GetBoundsInScreen().Intersects(
calendar_view_controller_->todays_date_cell_view()
->GetBoundsInScreen())) {
const int offset = calendar_view_controller_->todays_date_cell_view()
->GetBoundsInScreen()
.bottom() -
calendar_sliding_surface_->GetBoundsInScreen().y();
AnimateScrollByOffset(offset);
}
}
void CalendarView::RemoveUpNextView() {
if (!up_next_view_) {
return;
}
calendar_sliding_surface_->RemoveChildViewT(up_next_view_.get());
up_next_view_ = nullptr;
SetCalendarSlidingSurfaceBounds(event_list_view_);
ClipScrollViewHeight(event_list_view_ ? ScrollViewState::EVENT_LIST_SHOWING
: ScrollViewState::FULL_HEIGHT);
}
void CalendarView::OpenEventListForTodaysDate() {
const auto upcoming_events = calendar_view_controller_->UpcomingEvents();
const base::Time upcoming_event_start_time =
!upcoming_events.empty() ? upcoming_events.back().start_time().date_time()
: base::Time::Now();
if (!current_month_->has_today()) {
ResetToToday();
}
calendar_view_controller_->ShowEventListView(
/*selected_calendar_date_cell_view=*/calendar_view_controller_
->todays_date_cell_view(),
/*selected_date=*/upcoming_event_start_time,
/*row_index=*/calendar_view_controller_->today_row() - 1);
}
void CalendarView::ClipScrollViewHeight(ScrollViewState state_to_change_to) {
switch (state_to_change_to) {
case ScrollViewState::FULL_HEIGHT:
scroll_view_->ClipHeightTo(0, INT_MAX);
break;
case ScrollViewState::UP_NEXT_SHOWING:
scroll_view_->ClipHeightTo(
0, GetBoundsInScreen().bottom() -
scroll_view_->GetBoundsInScreen().y() -
up_next_view_->GetPreferredSize().height() +
calendar_utils::kUpNextOverlapInPx);
break;
case ScrollViewState::EVENT_LIST_SHOWING:
scroll_view_->ClipHeightTo(0, GetSingleVisibleRowHeight());
break;
}
}
void CalendarView::AnimateScrollByOffset(int offset) {
if (offset == 0) {
return;
}
if (IsAnimating()) {
RestoreMonthStatus();
scroll_view_->ScrollToPosition(scroll_view_->vertical_scroll_bar(),
scroll_view_->GetVisibleRect().y() + offset);
return;
}
SetShouldMonthsAnimateAndScrollEnabled(false);
gfx::Vector2dF moving_up_location = gfx::Vector2dF(0, -offset);
gfx::Transform month_moving;
month_moving.Translate(moving_up_location);
auto month_reporter = calendar_metrics::CreateAnimationReporter(
current_month_, kSmoothScrollMonthViewWhenShowingTodaysDateCell);
auto label_reporter = calendar_metrics::CreateAnimationReporter(
current_label_, kSmoothScrollLabelViewWhenShowingTodaysDateCell);
views::AnimationBuilder()
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.OnEnded(base::BindOnce(&CalendarView::OnAnimateScrollByOffsetComplete,
weak_factory_.GetWeakPtr(), offset))
.OnAborted(base::BindOnce(&CalendarView::OnAnimateScrollByOffsetComplete,
weak_factory_.GetWeakPtr(), offset))
.Once()
.SetDuration(kAnimationDurationForClosingEvents)
.SetTransform(current_month_, month_moving,
gfx::Tween::ACCEL_20_DECEL_100)
.SetTransform(current_label_, month_moving,
gfx::Tween::ACCEL_20_DECEL_100)
.SetTransform(next_label_, month_moving, gfx::Tween::ACCEL_20_DECEL_100)
.SetTransform(next_month_, month_moving, gfx::Tween::ACCEL_20_DECEL_100)
.SetTransform(next_next_label_, month_moving,
gfx::Tween::ACCEL_20_DECEL_100)
.SetTransform(next_next_month_, month_moving,
gfx::Tween::ACCEL_20_DECEL_100);
}
void CalendarView::OnAnimateScrollByOffsetComplete(int offset) {
if (is_destroying_) {
return;
}
SetShouldMonthsAnimateAndScrollEnabled(true);
RestoreMonthStatus();
scroll_view_->ScrollToPosition(scroll_view_->vertical_scroll_bar(),
scroll_view_->GetVisibleRect().y() + offset);
}
int CalendarView::GetSingleVisibleRowHeight() {
return features::IsCalendarJellyEnabled()
? calendar_view_controller_->row_height() +
kCalendarEventListViewOpenMarginJelly
: calendar_view_controller_->row_height();
}
BEGIN_METADATA(CalendarView, views::View)
END_METADATA
} // namespace ash