0

Add scroll buttons to Calendar up next view

Main changes:
- Add scroll buttons for calendar up next view
- Show single event as full width with no scroll buttons
- Add scrolling animation when scroll buttons are pressed

Smaller changes / refactors:
- Fix "Up next" view not displaying on initial load
- Add translation screenshots
- Add docs
- Remove static FakeNow methods from tests

Screenshots and recordings:
- Single event scroll buttons hidden http://shortn/_H4w3y6uXf8
- Multi events scroll buttons visible http://shortn/_glwCpdUgP3
- Scroll buttons behaviour http://shortn/_Ru7YX4RD9H

Bug: b:258649456
Change-Id: I1ad2b6f304eaf977857f4ade4f5aed900c930099
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4051265
Reviewed-by: Jiaming Cheng <jiamingc@chromium.org>
Commit-Queue: Sam Cackett <samcackett@google.com>
Cr-Commit-Position: refs/heads/main@{#1079754}
This commit is contained in:
Sam Cackett
2022-12-06 15:07:24 +00:00
committed by Chromium LUCI CQ
parent 7a815e1559
commit 28f7f35c3f
10 changed files with 803 additions and 98 deletions

@ -4213,6 +4213,14 @@ Connect your device to power.
Up next
</message>
<message name="IDS_ASH_CALENDAR_UP_NEXT_SCROLL_LEFT_BUTTON" desc="The accessible/tooltip description of the calendar up next scroll left button.">
Scroll left
</message>
<message name="IDS_ASH_CALENDAR_UP_NEXT_SCROLL_RIGHT_BUTTON" desc="The accessible/tooltip description of the calendar up next scroll right button.">
Scroll right
</message>
<message name="IDS_ASH_STATUS_TRAY_PROGRESS_BAR_ACCESSIBLE_NAME" desc="The accessible name for the progress bar shown in the status tray.">
Loading
</message>

@ -0,0 +1 @@
37b51ea4c7c2ad8c4ce1c212650c17b8a36786ff

@ -0,0 +1 @@
97479ecac269dff3243683847ecfa401011131cb

@ -141,12 +141,13 @@ class ASH_EXPORT CalendarModel : public SessionObserver {
friend class CalendarModelTest;
friend class CalendarMonthViewFetchTest;
friend class CalendarMonthViewTest;
friend class CalendarUpNextViewAnimationTest;
friend class CalendarUpNextViewPixelTest;
friend class CalendarUpNextViewTest;
friend class CalendarViewAnimationTest;
friend class CalendarViewEventListViewTest;
friend class CalendarViewTest;
friend class CalendarViewWithJellyEnabledTest;
friend class CalendarUpNextViewPixelTest;
friend class CalendarUpNextViewTest;
friend class GlanceablesTest;
// Checks if the event has allowed statuses and is eligible for insertion.

@ -21,9 +21,10 @@ namespace {
std::unique_ptr<google_apis::calendar::CalendarEvent> CreateEvent(
const base::Time start_time,
const base::Time end_time,
const char* summary = "Event with long name that should ellipsis",
bool all_day_event = false) {
return calendar_test_utils::CreateEvent(
"id_0", "Event with long name that should ellipsis", start_time, end_time,
"id_0", summary, start_time, end_time,
google_apis::calendar::CalendarEvent::EventStatus::kConfirmed,
google_apis::calendar::CalendarEvent::ResponseStatus::kAccepted,
all_day_event);
@ -73,15 +74,39 @@ class CalendarUpNextViewPixelTest : public AshTestBase {
views::Widget* Widget() { return widget_.get(); }
const views::View* GetHeaderView() { return up_next_view_->header_view_; }
const views::View* GetHeaderButtonContainerView() {
return GetHeaderView()->children()[1];
}
const views::View* GetScrollRightButton() {
return GetHeaderButtonContainerView()->children()[1];
}
void PressScrollRightButton() { PressScrollButton(GetScrollRightButton()); }
// End the scrolling animation.
void EndScrollingAnimation() { up_next_view_->scrolling_animation_->End(); }
private:
void PressScrollButton(const views::View* button) {
auto* event_generator = GetEventGenerator();
event_generator->MoveMouseTo(button->GetBoundsInScreen().CenterPoint());
event_generator->ClickLeftButton();
// End the scrolling animation immediately so the pixel test images aren't
// in an animating state. If we don't do this, the test images are taken
// almost immediately and are incorrect.
EndScrollingAnimation();
}
std::unique_ptr<views::Widget> widget_;
CalendarUpNextView* up_next_view_ = nullptr;
std::unique_ptr<CalendarViewController> controller_;
};
TEST_F(
CalendarUpNextViewPixelTest,
GivenASingleUpcomingEvent_WhenUpNextViewIsCreated_ThenShouldDisplaySingleUpcomingEvent) {
TEST_F(CalendarUpNextViewPixelTest,
ShouldShowSingleEventTakingUpFullWidthOfParentView) {
// Set time override.
base::subtle::ScopedTimeClockOverrides time_override(
[]() { return base::subtle::TimeNowIgnoringOverride().LocalMidnight(); },
@ -98,12 +123,11 @@ TEST_F(
CreateCalendarUpNextView(std::move(events));
EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
"calendar_up_next_single_upcoming_event.rev_0", Widget()));
"calendar_up_next_single_upcoming_event.rev_1", Widget()));
}
TEST_F(
CalendarUpNextViewPixelTest,
GivenThreeUpcomingEvents_WhenUpNextViewIsCreated_ThenShouldDisplayMultipleEventsInScrollView) {
TEST_F(CalendarUpNextViewPixelTest,
ShouldShowMultipleEventsInHorizontalScrollView) {
// Set time override.
base::subtle::ScopedTimeClockOverrides time_override(
[]() { return base::subtle::TimeNowIgnoringOverride().LocalMidnight(); },
@ -122,7 +146,36 @@ TEST_F(
CreateCalendarUpNextView(std::move(events));
EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
"calendar_up_next_multiple_upcoming_events.rev_0", Widget()));
"calendar_up_next_multiple_upcoming_events.rev_1", Widget()));
}
TEST_F(
CalendarUpNextViewPixelTest,
ShouldMakeSecondEventFullyVisibleAndLeftAligned_WhenScrollRightButtonIsPressed) {
// Set time override.
base::subtle::ScopedTimeClockOverrides time_override(
[]() { return base::subtle::TimeNowIgnoringOverride().LocalMidnight(); },
nullptr, nullptr);
// Add 3 events starting in 10 mins.
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
auto start_time = base::subtle::TimeNowIgnoringOverride().LocalMidnight() +
base::Minutes(10);
auto end_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() + base::Hours(1);
events.push_back(CreateEvent(start_time, end_time, "First event"));
events.push_back(CreateEvent(start_time, end_time, "Second event"));
events.push_back(CreateEvent(start_time, end_time, "Third event"));
events.push_back(CreateEvent(start_time, end_time, "Fourth event"));
CreateCalendarUpNextView(std::move(events));
PressScrollRightButton();
EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
"calendar_up_next_multiple_upcoming_events_press_scroll_right_button.rev_"
"0",
Widget()));
}
} // namespace ash

@ -4,20 +4,24 @@
#include "ash/system/time/calendar_up_next_view.h"
#include <algorithm>
#include <memory>
#include "ash/bubble/bubble_utils.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/icon_button.h"
#include "ash/system/time/calendar_event_list_item_view_jelly.h"
#include "ash/system/time/calendar_utils.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/color/color_provider.h"
#include "ui/gfx/text_constants.h"
#include "ui/views/background.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/metadata/view_factory_internal.h"
namespace ash {
@ -25,13 +29,87 @@ namespace {
constexpr int kContainerInsets = 12;
constexpr int kBackgroundRadius = 12;
constexpr int kBetweenChildSpacing = 8;
constexpr int kMaxEventListItemWidth = 160;
constexpr int kFullWidth = 0;
constexpr int kMaxItemWidth = 160;
constexpr int kHeaderBetweenChildSpacing = 14;
constexpr int kHeaderButtonsBetweenChildSpacing = 28;
// Helper class for managing scrolling animations.
class ScrollingAnimation : public gfx::LinearAnimation,
public gfx::AnimationDelegate {
public:
explicit ScrollingAnimation(
views::View* contents_view,
gfx::AnimationContainer* bounds_animator_container,
base::TimeDelta duration,
const gfx::Rect start_visible_rect,
const gfx::Rect end_visible_rect)
: gfx::LinearAnimation(duration,
gfx::LinearAnimation::kDefaultFrameRate,
this),
contents_view_(contents_view),
start_visible_rect_(start_visible_rect),
end_visible_rect_(end_visible_rect) {
SetContainer(bounds_animator_container);
}
ScrollingAnimation(const ScrollingAnimation&) = delete;
ScrollingAnimation& operator=(const ScrollingAnimation&) = delete;
~ScrollingAnimation() override = default;
void AnimateToState(double state) override {
gfx::Rect intermediary_rect(
start_visible_rect_.x() +
(end_visible_rect_.x() - start_visible_rect_.x()) * state,
start_visible_rect_.y(), start_visible_rect_.width(),
start_visible_rect_.height());
contents_view_->ScrollRectToVisible(intermediary_rect);
}
void AnimationEnded(const gfx::Animation* animation) override {
contents_view_->ScrollRectToVisible(end_visible_rect_);
}
void AnimationCanceled(const gfx::Animation* animation) override {
AnimationEnded(animation);
}
private:
// Owned by views hierarchy.
const raw_ptr<views::View> contents_view_;
const gfx::Rect start_visible_rect_;
const gfx::Rect end_visible_rect_;
};
views::Builder<views::Label> CreateHeaderLabel() {
return views::Builder<views::Label>(bubble_utils::CreateLabel(
bubble_utils::TypographyStyle::kButton2,
l10n_util::GetStringUTF16(IDS_ASH_CALENDAR_UP_NEXT)));
return views::Builder<views::Label>(
bubble_utils::CreateLabel(
bubble_utils::TypographyStyle::kButton2,
l10n_util::GetStringUTF16(IDS_ASH_CALENDAR_UP_NEXT)))
.SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
}
bool IsRightScrollButtonEnabled(views::ScrollView* scroll_view) {
const int contents_width =
scroll_view->contents()->GetContentsBounds().width();
const int scroll_position = scroll_view->GetVisibleRect().x();
const int scroll_view_width = scroll_view->width();
return (contents_width > scroll_view_width) &&
(scroll_position < (contents_width - scroll_view_width));
}
// Returns the index of the first (left-most) visible (partially or wholly)
// child in the ScrollView.
int GetFirstVisibleChildIndex(std::vector<views::View*> event_views,
views::View* scroll_view) {
for (size_t i = 0; i < event_views.size(); ++i) {
auto* child = event_views[i];
if (scroll_view->GetBoundsInScreen().Intersects(child->GetBoundsInScreen()))
return i;
}
return 0;
}
} // namespace
@ -42,15 +120,50 @@ CalendarUpNextView::CalendarUpNextView(
header_view_(AddChildView(std::make_unique<views::View>())),
scroll_view_(AddChildView(std::make_unique<views::ScrollView>(
views::ScrollView::ScrollWithLayers::kEnabled))),
content_view_(
scroll_view_->SetContents(std::make_unique<views::View>())) {
content_view_(scroll_view_->SetContents(std::make_unique<views::View>())),
bounds_animator_(this) {
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical, gfx::Insets(kContainerInsets),
kBetweenChildSpacing));
calendar_utils::kUpNextBetweenChildSpacing));
header_view_->SetLayoutManager(std::make_unique<views::FlexLayout>());
header_view_->AddChildView(CreateHeaderLabel().Build());
if (!gfx::Animation::ShouldRenderRichAnimation())
bounds_animator_.SetAnimationDuration(base::TimeDelta());
on_contents_scrolled_subscription_ =
scroll_view_->AddContentsScrolledCallback(
base::BindRepeating(&CalendarUpNextView::ToggleScrollButtonState,
base::Unretained(this)));
// Header.
auto* header_layout_manager =
header_view_->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal, gfx::Insets(),
kHeaderBetweenChildSpacing));
// Header label.
auto* header_label = header_view_->AddChildView(CreateHeaderLabel().Build());
header_layout_manager->SetFlexForView(header_label, 1);
// Header buttons.
auto button_container =
views::Builder<views::View>()
.SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal, gfx::Insets(),
kHeaderButtonsBetweenChildSpacing))
.Build();
left_scroll_button_ =
button_container->AddChildView(std::make_unique<IconButton>(
base::BindRepeating(&CalendarUpNextView::OnScrollLeftButtonPressed,
base::Unretained(this)),
IconButton::Type::kXSmallFloating, &kCaretLeftIcon,
IDS_ASH_CALENDAR_UP_NEXT_SCROLL_LEFT_BUTTON));
right_scroll_button_ =
button_container->AddChildView(std::make_unique<IconButton>(
base::BindRepeating(&CalendarUpNextView::OnScrollRightButtonPressed,
base::Unretained(this)),
IconButton::Type::kXSmallFloating, &kCaretRightIcon,
IDS_ASH_CALENDAR_UP_NEXT_SCROLL_RIGHT_BUTTON));
header_view_->AddChildView(std::move(button_container));
// Scroll view.
scroll_view_->SetAllowKeyboardScrolling(false);
scroll_view_->SetBackgroundColor(absl::nullopt);
scroll_view_->SetDrawOverflowIndicator(false);
@ -58,28 +171,37 @@ CalendarUpNextView::CalendarUpNextView(
views::ScrollView::ScrollBarMode::kHiddenButEnabled);
scroll_view_->SetTreatAllScrollEventsAsHorizontal(true);
content_view_->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal, gfx::Insets(),
kBetweenChildSpacing));
// Contents.
const auto events = calendar_view_controller_->UpcomingEvents();
auto* content_layout_manager =
content_view_->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal, gfx::Insets(),
calendar_utils::kUpNextBetweenChildSpacing));
UpdateEvents();
// Populate the contents of the scroll view.
UpdateEvents(events, content_layout_manager);
}
CalendarUpNextView::~CalendarUpNextView() = default;
void CalendarUpNextView::Layout() {
// For some reason the `content_view_` is constrained to the `scroll_view_`
// width and so it isn't scrollable. This seems to be a problem with
// horizontal `ScrollView`s as this doesn't happen if you make this view
// vertically scrollable. To make the content scrollable, we need to set it's
// preferred size here so it's bigger than the `scroll_view_` and
// therefore scrolls.
// For some reason the `content_view_` is constrained to the
// `scroll_view_` width and so it isn't scrollable. This seems to be a
// problem with horizontal `ScrollView`s as this doesn't happen if you
// make this view vertically scrollable. To make the content
// scrollable, we need to set it's preferred size here so it's bigger
// than the `scroll_view_` and therefore scrolls. See
// https://crbug.com/1384131.
if (content_view_)
content_view_->SizeToPreferredSize();
// `content_view_` is a child of this class so we need to Layout after
// changing its width.
views::View::Layout();
// After laying out the `content_view_`, we need to set the initial scroll
// button state.
ToggleScrollButtonState();
}
void CalendarUpNextView::OnThemeChanged() {
@ -89,13 +211,35 @@ void CalendarUpNextView::OnThemeChanged() {
kBackgroundRadius));
}
void CalendarUpNextView::UpdateEvents() {
void CalendarUpNextView::UpdateEvents(
const std::list<google_apis::calendar::CalendarEvent>& events,
views::BoxLayout* content_layout_manager) {
content_view_->RemoveAllChildViews();
std::list<google_apis::calendar::CalendarEvent> events =
calendar_view_controller_->UpcomingEvents();
const auto now = base::Time::NowFromSystemTime();
auto now = base::Time::NowFromSystemTime();
// Single events are displayed filling the whole width of the tray.
if (events.size() == 1) {
const auto event = events.back();
auto* child_view = content_view_->AddChildView(
std::make_unique<CalendarEventListItemViewJelly>(
calendar_view_controller_,
SelectedDateParams{now, now.UTCMidnight(), now.LocalMidnight()},
/*event=*/event, /*round_top_corners=*/true,
/*round_bottom_corners=*/true,
/*max_width=*/kFullWidth));
content_layout_manager->SetFlexForView(child_view, 1);
// Hide scroll buttons if we have a single event.
left_scroll_button_->SetVisible(false);
right_scroll_button_->SetVisible(false);
return;
}
// Multiple events are displayed in a scroll view of events with a max item
// width. Longer event names will have an ellipsis applied.
for (auto& event : events) {
content_view_->AddChildView(
std::make_unique<CalendarEventListItemViewJelly>(
@ -103,8 +247,104 @@ void CalendarUpNextView::UpdateEvents() {
SelectedDateParams{now, now.UTCMidnight(), now.LocalMidnight()},
/*event=*/event, /*round_top_corners=*/true,
/*round_bottom_corners=*/true,
/*max_width=*/kMaxEventListItemWidth));
/*max_width=*/kMaxItemWidth));
}
// Show scroll buttons if we have multiple events.
left_scroll_button_->SetVisible(true);
right_scroll_button_->SetVisible(true);
}
void CalendarUpNextView::OnScrollLeftButtonPressed(const ui::Event& event) {
const Views& event_views = content_view_->children();
if (event_views.empty())
return;
const int first_visible_child_index =
GetFirstVisibleChildIndex(event_views, scroll_view_);
views::View* first_visible_child = event_views[first_visible_child_index];
// If first visible child is partially visible, then just scroll to make it
// visible.
if (first_visible_child->GetVisibleBounds().width() !=
first_visible_child->GetContentsBounds().width()) {
const auto offset = first_visible_child->GetBoundsInScreen().x() -
scroll_view_->GetBoundsInScreen().x();
ScrollViewByOffset(offset);
return;
}
// Otherwise, find the child before that and scroll to it.
const int previous_child_index = first_visible_child_index - 1;
const int index = std::max(0, previous_child_index);
views::View* previous_child = event_views[index];
const auto offset = previous_child->GetBoundsInScreen().x() -
scroll_view_->GetBoundsInScreen().x();
ScrollViewByOffset(offset);
}
void CalendarUpNextView::OnScrollRightButtonPressed(const ui::Event& event) {
const Views& event_views = content_view_->children();
if (event_views.empty())
return;
const int first_visible_child_index =
GetFirstVisibleChildIndex(event_views, scroll_view_);
// When scrolling right, the next event should be aligned to the left of the
// scroll view. The amount to offset is calculated by getting the visible
// bounds of the first visible child + the between child spacing. Using the
// visible bounds means this handles partially or fully visible views and we
// scroll past them i.e. the amount of space the first visible event takes up
// so the next one lines up nicely.
const int first_child_offset =
(event_views[first_visible_child_index]->GetVisibleBounds().width() +
calendar_utils::kUpNextBetweenChildSpacing);
// Calculate the max scroll position based on how far along we've scrolled.
// `ScrollByOffset` will go way past the size of the contents so we need to
// constrain it to go no further than the end of the content view.
const int max_scroll_position = content_view_->GetContentsBounds().width() -
scroll_view_->GetVisibleRect().right();
const int offset = std::min(max_scroll_position, first_child_offset);
ScrollViewByOffset(offset);
}
void CalendarUpNextView::ToggleScrollButtonState() {
// Enable the scroll view buttons if there is a position to scroll to.
left_scroll_button_->SetEnabled(scroll_view_->GetVisibleRect().x() > 0);
right_scroll_button_->SetEnabled(IsRightScrollButtonEnabled(scroll_view_));
}
void CalendarUpNextView::ScrollViewByOffset(int offset) {
absl::optional<gfx::Rect> visible_content_rect =
scroll_view_->GetVisibleRect();
if (!visible_content_rect.has_value() || offset == 0)
return;
// Set the `start_edge` depending on the offset.
// If the offset is negative ie. we're scrolling left, we should use the x
// coordinate of the scroll viewport as the `start_edge` to base our offset
// on. If the offset is positive i.e. we're scrolling right, then we should
// use the right coordinate of the viewport.
int start_edge =
(offset > 0) ? visible_content_rect->right() : visible_content_rect->x();
AnimateScrollToShowXCoordinate(start_edge, start_edge + offset);
}
void CalendarUpNextView::AnimateScrollToShowXCoordinate(const int start_edge,
const int target_edge) {
if (scrolling_animation_)
scrolling_animation_->Stop();
scrolling_animation_ = std::make_unique<ScrollingAnimation>(
content_view_, bounds_animator_.container(),
bounds_animator_.GetAnimationDuration(),
/*start_visible_rect=*/gfx::Rect(start_edge, 0, 0, 0),
/*end_visible_rect=*/gfx::Rect(target_edge, 0, 0, 0));
scrolling_animation_->Start();
}
BEGIN_METADATA(CalendarUpNextView, views::View);

@ -7,8 +7,14 @@
#include "ash/ash_export.h"
#include "ash/system/time/calendar_view_controller.h"
#include "ui/views/animation/bounds_animator.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/view.h"
namespace views {
class BoxLayout;
}
namespace ash {
// This view displays a scrollable list of `CalendarEventListItemView` for the
@ -24,23 +30,58 @@ class ASH_EXPORT CalendarUpNextView : public views::View {
~CalendarUpNextView() override;
// views::View
void OnThemeChanged() override;
void Layout() override;
void OnThemeChanged() override;
private:
friend class CalendarUpNextViewTest;
friend class CalendarUpNextViewAnimationTest;
friend class CalendarUpNextViewPixelTest;
void UpdateEvents();
// Populates the scroll view with events.
void UpdateEvents(
const std::list<google_apis::calendar::CalendarEvent>& events,
views::BoxLayout* content_layout_manager);
// Callbacks for scroll buttons.
void OnScrollLeftButtonPressed(const ui::Event& event);
void OnScrollRightButtonPressed(const ui::Event& event);
// Toggles enabled / disabled states of the scroll buttons.
void ToggleScrollButtonState();
// Scrolls the scroll view by the given offset.
void ScrollViewByOffset(int offset);
// Takes two coordinates and animates the `content_view_` to move between
// them. Gives the effect of animating the horizontal `scroll_view_` smoothly
// moving upon the `left_scroll_button_` and `right_scroll_button_` presses.
void AnimateScrollToShowXCoordinate(const int start_edge,
const int target_edge);
// Owned by `CalendarView`.
CalendarViewController* calendar_view_controller_;
// Owned by `CalendarUpNextView`.
views::View* const header_view_;
views::Button* left_scroll_button_;
views::Button* right_scroll_button_;
views::ScrollView* const scroll_view_;
// The content of the horizontal `scroll_view`, which carries a list of
// `CalendarEventListItemView`.
views::View* const content_view_;
// Helper class for animating the `scroll_view_` when a scroll button is
// pressed.
std::unique_ptr<gfx::LinearAnimation> scrolling_animation_;
// Bounds animator used in the `scrolling_animation_` class.
views::BoundsAnimator bounds_animator_;
// Callback subscriptions.
base::CallbackListSubscription on_contents_scrolled_subscription_;
};
} // namespace ash

@ -8,18 +8,19 @@
#include "ash/system/model/system_tray_model.h"
#include "ash/system/time/calendar_unittest_utils.h"
#include "ash/system/time/calendar_view_controller.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/test/ash_test_base.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
namespace ash {
namespace {
std::unique_ptr<google_apis::calendar::CalendarEvent> CreateEvent(
const char* start_time,
const char* end_time,
const base::Time start_time,
const base::Time end_time,
bool all_day_event = false) {
return calendar_test_utils::CreateEvent(
"id_0", "summary_0", start_time, end_time,
@ -33,9 +34,9 @@ std::unique_ptr<google_apis::calendar::CalendarEvent> CreateEvent(
class CalendarUpNextViewTest : public AshTestBase {
public:
CalendarUpNextViewTest() = default;
CalendarUpNextViewTest(const CalendarUpNextViewTest&) = delete;
CalendarUpNextViewTest& operator=(const CalendarUpNextViewTest&) = delete;
~CalendarUpNextViewTest() override = default;
explicit CalendarUpNextViewTest(
base::test::TaskEnvironment::TimeSource time_source)
: AshTestBase(time_source) {}
void SetUp() override {
AshTestBase::SetUp();
@ -43,75 +44,428 @@ class CalendarUpNextViewTest : public AshTestBase {
}
void TearDown() override {
up_next_view_.reset();
controller_.reset();
AshTestBase::TearDown();
}
void CreateUpNextView(
base::Time date,
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events) {
up_next_view_.reset();
if (!widget_)
widget_ = CreateFramelessTestWidget();
// Mock events being fetched.
Shell::Get()->system_tray_model()->calendar_model()->OnEventsFetched(
calendar_utils::GetStartOfMonthUTC(date),
calendar_utils::GetStartOfMonthUTC(
base::subtle::TimeNowIgnoringOverride().LocalMidnight()),
google_apis::ApiErrorCode::HTTP_SUCCESS,
calendar_test_utils::CreateMockEventList(std::move(events)).get());
up_next_view_ = std::make_unique<CalendarUpNextView>(controller_.get());
auto up_next_view = std::make_unique<CalendarUpNextView>(controller_.get());
up_next_view_ = widget_->SetContentsView(std::move(up_next_view));
// Set the widget to reflect the CalendarUpNextView size in reality. If we
// don't then the view will never be scrollable.
widget_->SetSize(
gfx::Size(kTrayMenuWidth, up_next_view_->GetPreferredSize().height()));
}
const views::View* GetHeaderView() { return up_next_view_->header_view_; }
const views::Label* GetHeaderLabel() {
return static_cast<views::Label*>(
up_next_view_->header_view_->children()[0]);
return static_cast<views::Label*>(GetHeaderView()->children()[0]);
}
const views::View* GetContentsView() {
return static_cast<views::View*>(up_next_view_->content_view_);
const views::View* GetContentsView() { return up_next_view_->content_view_; }
const views::ScrollView* GetScrollView() {
return up_next_view_->scroll_view_;
}
static base::Time FakeTimeNow() { return fake_time_; }
static void SetFakeNow(base::Time fake_now) { fake_time_ = fake_now; }
const views::View* GetScrollLeftButton() {
return up_next_view_->left_scroll_button_;
}
const views::View* GetScrollRightButton() {
return up_next_view_->right_scroll_button_;
}
virtual void PressScrollLeftButton() {
PressScrollButton(GetScrollLeftButton());
}
virtual void PressScrollRightButton() {
PressScrollButton(GetScrollRightButton());
}
int ScrollPosition() { return GetScrollView()->GetVisibleRect().x(); }
void ScrollHorizontalPositionTo(int position_in_px) {
up_next_view_->scroll_view_->ScrollToPosition(
up_next_view_->scroll_view_->horizontal_scroll_bar(), position_in_px);
}
// End the scrolling animation.
void EndScrollingAnimation() { up_next_view_->scrolling_animation_->End(); }
CalendarViewController* controller() { return controller_.get(); }
CalendarUpNextView* up_next_view() { return up_next_view_.get(); }
CalendarUpNextView* up_next_view() { return up_next_view_; }
private:
std::unique_ptr<CalendarUpNextView> up_next_view_;
virtual void PressScrollButton(const views::View* button) {
auto* event_generator = GetEventGenerator();
event_generator->MoveMouseTo(button->GetBoundsInScreen().CenterPoint());
event_generator->ClickLeftButton();
// End the scrolling animation immediately so tests can assert the results
// of scrolling. If we don't do this, the test assertions run immediately
// (and fail) due to animations concurrently running.
EndScrollingAnimation();
}
std::unique_ptr<views::Widget> widget_;
CalendarUpNextView* up_next_view_;
std::unique_ptr<CalendarViewController> controller_;
base::test::ScopedFeatureList features_;
static base::Time fake_time_;
};
base::Time CalendarUpNextViewTest::fake_time_;
TEST_F(CalendarUpNextViewTest,
GivenUpcomingEvents_WhenUpNextViewIsCreated_ThenShowEvents) {
base::Time date;
ASSERT_TRUE(base::Time::FromString("22 Nov 2021 09:00 GMT", &date));
TEST_F(CalendarUpNextViewTest, ShouldShowMultipleUpcomingEvents) {
// Set time override.
SetFakeNow(date);
base::subtle::ScopedTimeClockOverrides time_override(
&CalendarUpNextViewTest::FakeTimeNow, /*time_ticks_override=*/nullptr,
/*thread_ticks_override=*/nullptr);
// Event starts in 10 mins.
const char* event_in_ten_mins_start_time_string = "22 Nov 2021 09:10 GMT";
const char* event_in_ten_mins_end_time_string = "22 Nov 2021 10:00 GMT";
// Event in progress.
const char* event_in_progress_start_time_string = "22 Nov 2021 08:30 GMT";
const char* event_in_progress_end_time_string = "22 Nov 2021 09:30 GMT";
[]() { return base::subtle::TimeNowIgnoringOverride().LocalMidnight(); },
nullptr, nullptr);
// Add event starting in 10 mins.
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
events.push_back(CreateEvent(event_in_ten_mins_start_time_string,
event_in_ten_mins_end_time_string));
events.push_back(CreateEvent(event_in_progress_start_time_string,
event_in_progress_end_time_string));
auto event_in_ten_mins_start_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() +
base::Minutes(10);
auto event_in_ten_mins_end_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() + base::Hours(1);
CreateUpNextView(date, std::move(events));
// Add event that's in progress.
auto event_in_progress_start_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() -
base::Minutes(30);
auto event_in_progress_end_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() +
base::Minutes(30);
events.push_back(
CreateEvent(event_in_ten_mins_start_time, event_in_ten_mins_end_time));
events.push_back(
CreateEvent(event_in_progress_start_time, event_in_progress_end_time));
CreateUpNextView(std::move(events));
EXPECT_EQ(GetHeaderLabel()->GetText(), u"Up next");
EXPECT_EQ(GetContentsView()->children().size(), size_t(2));
}
TEST_F(CalendarUpNextViewTest,
ShouldShowSingleEventTakingUpFullWidthOfParentView) {
// Set time override.
base::subtle::ScopedTimeClockOverrides time_override(
[]() { return base::subtle::TimeNowIgnoringOverride().LocalMidnight(); },
nullptr, nullptr);
// Add single event starting in 10 mins.
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
auto event_in_ten_mins_start_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() +
base::Minutes(10);
auto event_in_ten_mins_end_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() + base::Hours(1);
events.push_back(
CreateEvent(event_in_ten_mins_start_time, event_in_ten_mins_end_time));
CreateUpNextView(std::move(events));
EXPECT_EQ(GetContentsView()->children().size(), size_t(1));
EXPECT_EQ(GetContentsView()->children()[0]->width(),
GetScrollView()->width());
}
TEST_F(CalendarUpNextViewTest,
ShouldScrollLeftAndRightWhenScrollButtonsArePressed) {
// Set time override.
base::subtle::ScopedTimeClockOverrides time_override(
[]() { return base::subtle::TimeNowIgnoringOverride().LocalMidnight(); },
nullptr, nullptr);
// Add multiple events starting in 10 mins.
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
auto event_in_ten_mins_start_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() +
base::Minutes(10);
auto event_in_ten_mins_end_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() + base::Hours(1);
for (int i = 0; i < 5; ++i) {
events.push_back(
CreateEvent(event_in_ten_mins_start_time, event_in_ten_mins_end_time));
}
CreateUpNextView(std::move(events));
EXPECT_EQ(GetContentsView()->children().size(), size_t(5));
EXPECT_EQ(ScrollPosition(), 0);
// Press scroll right. We should scroll past the first event + margin.
const int first_event_width =
GetContentsView()->children()[0]->GetContentsBounds().width() +
calendar_utils::kUpNextBetweenChildSpacing;
PressScrollRightButton();
EXPECT_EQ(ScrollPosition(), first_event_width);
// Press scroll right again. We should scroll past the second event +
// margin.
const int second_event_width =
GetContentsView()->children()[1]->GetContentsBounds().width() +
calendar_utils::kUpNextBetweenChildSpacing;
PressScrollRightButton();
EXPECT_EQ(ScrollPosition(), first_event_width + second_event_width);
// Press scroll left. Now we should be back to being past the first event +
// margin.
PressScrollLeftButton();
EXPECT_EQ(ScrollPosition(), first_event_width);
// Press scroll left again. We should be back at the beginning of the scroll
// view.
PressScrollLeftButton();
EXPECT_EQ(ScrollPosition(), 0);
}
TEST_F(CalendarUpNextViewTest, ShouldHideScrollButtons_WhenOnlyOneEvent) {
// Set time override.
base::subtle::ScopedTimeClockOverrides time_override(
[]() { return base::subtle::TimeNowIgnoringOverride().LocalMidnight(); },
nullptr, nullptr);
// Add single event starting in 10 mins.
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
auto event_in_ten_mins_start_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() +
base::Minutes(10);
auto event_in_ten_mins_end_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() + base::Hours(1);
events.push_back(
CreateEvent(event_in_ten_mins_start_time, event_in_ten_mins_end_time));
CreateUpNextView(std::move(events));
EXPECT_EQ(GetContentsView()->children().size(), size_t(1));
EXPECT_EQ(ScrollPosition(), 0);
// With only one event, there won't be any room to scroll in either direction
// so the buttons should be hidden.
EXPECT_FALSE(GetScrollLeftButton()->GetVisible());
EXPECT_FALSE(GetScrollRightButton()->GetVisible());
}
TEST_F(CalendarUpNextViewTest, ShouldShowScrollButtons_WhenMultipleEvents) {
// Set time override.
base::subtle::ScopedTimeClockOverrides time_override(
[]() { return base::subtle::TimeNowIgnoringOverride().LocalMidnight(); },
nullptr, nullptr);
// Add multiple events starting in 10 mins.
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
auto event_in_ten_mins_start_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() +
base::Minutes(10);
auto event_in_ten_mins_end_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() + base::Hours(1);
for (int i = 0; i < 5; ++i) {
events.push_back(
CreateEvent(event_in_ten_mins_start_time, event_in_ten_mins_end_time));
}
CreateUpNextView(std::move(events));
EXPECT_EQ(GetContentsView()->children().size(), size_t(5));
// At the start the scroll left button should be disabled and visible.
EXPECT_EQ(ScrollPosition(), 0);
EXPECT_FALSE(GetScrollLeftButton()->GetEnabled());
EXPECT_TRUE(GetScrollLeftButton()->GetVisible());
EXPECT_TRUE(GetScrollRightButton()->GetEnabled());
EXPECT_TRUE(GetScrollRightButton()->GetVisible());
PressScrollRightButton();
// After scrolling right a bit, both buttons should be enabled and visible.
EXPECT_TRUE(GetScrollLeftButton()->GetEnabled());
EXPECT_TRUE(GetScrollLeftButton()->GetVisible());
EXPECT_TRUE(GetScrollRightButton()->GetEnabled());
EXPECT_TRUE(GetScrollRightButton()->GetVisible());
PressScrollRightButton();
PressScrollRightButton();
PressScrollRightButton();
// After scrolling to the end, the scroll right button should be disabled and
// visible.
EXPECT_TRUE(GetScrollLeftButton()->GetEnabled());
EXPECT_FALSE(GetScrollRightButton()->GetEnabled());
EXPECT_TRUE(GetScrollRightButton()->GetVisible());
}
// If we have a partially visible event view and the scroll left button is
// pressed, we should scroll to put the whole event into view, aligned to the
// start of the viewport.
// [---------------] <-- ScrollView viewport
// [-E1-] [---E2---] <-- Event 2 partially shown in the viewport.
// Press scroll left button.
// [---------------] <-- ScrollView viewport
// [-E1-] [---E2---] <-- Event 2 now fully shown in viewport.
TEST_F(
CalendarUpNextViewTest,
ShouldMakeCurrentOrPreviousEventFullyVisibleAndLeftAligned_WhenScrollLeftButtonIsPressed) {
// Set time override.
base::subtle::ScopedTimeClockOverrides time_override(
[]() { return base::subtle::TimeNowIgnoringOverride().LocalMidnight(); },
nullptr, nullptr);
// Add multiple events starting in 10 mins.
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
auto event_in_ten_mins_start_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() +
base::Minutes(10);
auto event_in_ten_mins_end_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() + base::Hours(1);
for (int i = 0; i < 5; ++i) {
events.push_back(
CreateEvent(event_in_ten_mins_start_time, event_in_ten_mins_end_time));
}
CreateUpNextView(std::move(events));
EXPECT_EQ(GetContentsView()->children().size(), size_t(5));
EXPECT_EQ(ScrollPosition(), 0);
// Scroll right so the second event is partially visible on the left of the
// scrollview.
ScrollHorizontalPositionTo(200);
ASSERT_EQ(ScrollPosition(), 200);
const views::View* second_event = GetContentsView()->children()[1];
// Assert second view is partially visible.
EXPECT_TRUE(second_event->GetVisibleBounds().width() <
second_event->GetContentsBounds().width());
// Press scroll left. We should scroll so that the second event is aligned to
// the start of the scroll view and fully visible. This is the equivalent
// position of being scrolled to the right of the width of the first event.
const int first_event_width =
GetContentsView()->children()[0]->GetContentsBounds().width() +
calendar_utils::kUpNextBetweenChildSpacing;
PressScrollLeftButton();
EXPECT_EQ(ScrollPosition(), first_event_width);
}
// If we have a partially visible event and the scroll right button is pressed,
// we should scroll to put the whole event into view, aligned to the start of
// the viewport.
// If we scroll right for a partially visible event view.
// [---------------] <-- ScrollView viewport
// [--E1--] [--E2--] <-- Event 2 partially shown in the viewport.
// Press scroll right button.
// [---------------] <-- ScrollView viewport
// [--E1--] [--E2--] <-- Event 2 now fully shown in the viewport.
TEST_F(
CalendarUpNextViewTest,
ShouldMakeNextEventFullyVisibleAndLeftAligned_WhenScrollRightButtonIsPressed) {
// Set time override.
base::subtle::ScopedTimeClockOverrides time_override(
[]() { return base::subtle::TimeNowIgnoringOverride().LocalMidnight(); },
nullptr, nullptr);
// Add multiple events starting in 10 mins.
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
auto event_in_ten_mins_start_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() +
base::Minutes(10);
auto event_in_ten_mins_end_time =
base::subtle::TimeNowIgnoringOverride().LocalMidnight() + base::Hours(1);
for (int i = 0; i < 5; ++i) {
events.push_back(
CreateEvent(event_in_ten_mins_start_time, event_in_ten_mins_end_time));
}
CreateUpNextView(std::move(events));
EXPECT_EQ(GetContentsView()->children().size(), size_t(5));
EXPECT_EQ(ScrollPosition(), 0);
ScrollHorizontalPositionTo(100);
ASSERT_EQ(ScrollPosition(), 100);
const views::View* first_event = GetContentsView()->children()[0];
// Assert first view is partially visible.
EXPECT_TRUE(first_event->GetVisibleBounds().width() <
first_event->GetContentsBounds().width());
// Press scroll right. We should scroll past the first event + margin to
// show the second event, aligned to the start of the scroll view.
const int first_event_width = first_event->GetContentsBounds().width() +
calendar_utils::kUpNextBetweenChildSpacing;
PressScrollRightButton();
EXPECT_EQ(ScrollPosition(), first_event_width);
}
class CalendarUpNextViewAnimationTest : public CalendarUpNextViewTest {
public:
CalendarUpNextViewAnimationTest()
: CalendarUpNextViewTest(
base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
void PressScrollLeftButton() override {
PressScrollButton(GetScrollLeftButton());
}
void PressScrollRightButton() override {
PressScrollButton(GetScrollRightButton());
}
bool IsAnimating() {
return up_next_view()->scrolling_animation_ &&
up_next_view()->scrolling_animation_->is_animating();
}
const base::TimeDelta kAnimationStartBufferDuration = base::Milliseconds(50);
const base::TimeDelta kAnimationFinishedDuration = base::Seconds(1);
private:
void PressScrollButton(const views::View* button) override {
auto* event_generator = GetEventGenerator();
event_generator->MoveMouseTo(button->GetBoundsInScreen().CenterPoint());
event_generator->ClickLeftButton();
}
};
TEST_F(CalendarUpNextViewAnimationTest,
ShouldAnimateScrollView_WhenScrollButtonsArePressed) {
// Add multiple events starting in 10 mins.
std::list<std::unique_ptr<google_apis::calendar::CalendarEvent>> events;
auto event_in_ten_mins_start_time = base::Time::Now() + base::Minutes(10);
auto event_in_ten_mins_end_time = base::Time::Now() + base::Hours(1);
for (int i = 0; i < 5; ++i) {
events.push_back(
CreateEvent(event_in_ten_mins_start_time, event_in_ten_mins_end_time));
}
CreateUpNextView(std::move(events));
EXPECT_FALSE(IsAnimating());
PressScrollRightButton();
task_environment()->FastForwardBy(kAnimationStartBufferDuration);
EXPECT_TRUE(IsAnimating());
task_environment()->FastForwardBy(kAnimationFinishedDuration);
EXPECT_FALSE(IsAnimating());
PressScrollLeftButton();
task_environment()->FastForwardBy(kAnimationStartBufferDuration);
EXPECT_TRUE(IsAnimating());
task_environment()->FastForwardBy(kAnimationFinishedDuration);
EXPECT_FALSE(IsAnimating());
}
} // namespace ash

@ -83,6 +83,9 @@ constexpr int kMaxNumNonPrunableMonths = 2 * kNumSurroundingMonthsCached + 1;
// kMaxNumNonPrunableMonths is the total maximum number of cached months.
constexpr int kMaxNumPrunableMonths = 20;
// Between child spacing for `CalendarUpNextView`.
constexpr int kUpNextBetweenChildSpacing = 8;
// Checks if the `selected_date` is local time today.
bool IsToday(const base::Time selected_date);

@ -1795,27 +1795,30 @@ void CalendarView::SetEventListViewBounds() {
}
void CalendarView::MaybeShowUpNextView() {
if (features::IsCalendarJellyEnabled() && EventsFetchComplete() &&
!calendar_view_controller_->UpcomingEvents().empty()) {
if (up_next_view_)
return;
up_next_view_ = AddChildView(
std::make_unique<CalendarUpNextView>(calendar_view_controller_.get()));
} else {
if (!features::IsCalendarJellyEnabled() || !EventsFetchComplete() ||
calendar_view_controller_->UpcomingEvents().empty()) {
RemoveUpNextView();
return;
}
if (up_next_view_)
return;
up_next_view_ = AddChildView(
std::make_unique<CalendarUpNextView>(calendar_view_controller_.get()));
InvalidateLayout();
}
void CalendarView::RemoveUpNextView() {
if (up_next_view_) {
RemoveChildViewT(up_next_view_);
up_next_view_ = nullptr;
// If the up next view is deleted whilst the calendar is still open, e.g.
// time has passed and an event no longer meets 'upcoming' criteria, then
// the calendar view needs to relayout after removing the upnext view.
InvalidateLayout();
}
if (!up_next_view_)
return;
RemoveChildViewT(up_next_view_);
up_next_view_ = nullptr;
// If the up next view is deleted whilst the calendar is still open, e.g.
// time has passed and an event no longer meets 'upcoming' criteria, then
// the calendar view needs to relayout after removing the upnext view.
InvalidateLayout();
}
BEGIN_METADATA(CalendarView, views::View)