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:

committed by
Chromium LUCI CQ

parent
7a815e1559
commit
28f7f35c3f
@ -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)
|
||||
|
Reference in New Issue
Block a user