[go: nahoru, domu]

blob: 8494342ea751086bccf71eb0647d3928fd459104 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/system/time/calendar_month_view.h"
#include "ash/public/cpp/ash_typography.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_provider.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/time/calendar_metrics.h"
#include "ash/system/time/calendar_model.h"
#include "ash/system/time/calendar_utils.h"
#include "ash/system/time/calendar_view_controller.h"
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/check.h"
#include "base/containers/contains.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/layer.h"
#include "ui/events/event.h"
#include "ui/gfx/canvas.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/layout/table_layout.h"
namespace ash {
namespace {
// The thickness of the border.
constexpr int kBorderLineThickness = 2;
// The radius used to draw the border.
constexpr float kBorderRadius = 21.f;
// The default radius used to draw rounded today's circle.
constexpr float kTodayRoundedRadius = 22.f;
// The radius used to draw rounded today's circle when focused.
constexpr float kTodayFocusedRoundedRadius = 18.f;
// Radius of the small dot displayed on a CalendarDateCellView if events are
// present for that day.
constexpr float kEventsPresentRoundedRadius = 1.f;
// The gap padding between the date and the indicator.
constexpr int kGapBetweenDateAndIndicator = 1;
// Move to the next day. Both the column and the current date are moved to the
// next one.
void MoveToNextDay(int& column,
base::Time& current_date,
base::Time& local_current_date,
base::Time::Exploded& current_date_exploded) {
// Using 30 hours to make sure the date is moved to the next day, since there
// are daylight saving days which have more than 24 hours in a day.
// `base::Days(1)` cannot be used, because it is 24 hours.
//
// Also using the `current_date_exploded` hours to calculate the local
// midnight.
int hours = current_date_exploded.hour;
current_date += base::Hours(30 - hours);
local_current_date += base::Hours(30 - hours);
local_current_date.UTCExplode(&current_date_exploded);
column = (column + 1) % calendar_utils::kDateInOneWeek;
}
} // namespace
CalendarDateCellView::CalendarDateCellView(
CalendarViewController* calendar_view_controller,
base::Time date,
base::TimeDelta time_difference,
bool is_grayed_out_date,
int row_index,
bool is_fetched)
: views::LabelButton(
views::Button::PressedCallback(
base::BindRepeating(&CalendarDateCellView::OnDateCellActivated,
base::Unretained(this))),
calendar_utils::GetDayIntOfMonth(date + time_difference),
CONTEXT_CALENDAR_DATE),
date_(date),
grayed_out_(is_grayed_out_date),
row_index_(row_index),
is_fetched_(is_fetched),
time_difference_(time_difference),
calendar_view_controller_(calendar_view_controller) {
SetHorizontalAlignment(gfx::ALIGN_CENTER);
SetBorder(views::CreateEmptyBorder(calendar_utils::kDateCellInsets));
label()->SetElideBehavior(gfx::NO_ELIDE);
label()->SetSubpixelRenderingEnabled(false);
views::FocusRing::Remove(this);
DisableFocus();
if (!grayed_out_) {
if (calendar_utils::IsActiveUser() && is_fetched_) {
event_number_ = calendar_view_controller_->GetEventNumber(date_);
}
SetTooltipAndAccessibleName();
is_selected_ = calendar_view_controller->selected_date_cell_view() == this;
calendar_utils::IsTheSameDay(date_,
calendar_view_controller_->selected_date());
}
scoped_calendar_view_controller_observer_.Observe(calendar_view_controller_);
}
CalendarDateCellView::~CalendarDateCellView() = default;
void CalendarDateCellView::OnThemeChanged() {
views::View::OnThemeChanged();
// Gray-out the date that is not in the current month.
SetEnabledTextColors(grayed_out_ ? calendar_utils::GetDisabledTextColor()
: calendar_utils::GetPrimaryTextColor());
}
// Draws the background for this date. Note that this includes not only the
// circular fill (if any), but also the border (if focused) and text color. If
// this is a grayed out date, which is shown in its previous/next month, this
// background won't be drawn.
void CalendarDateCellView::OnPaintBackground(gfx::Canvas* canvas) {
if (grayed_out_)
return;
const AshColorProvider* color_provider = AshColorProvider::Get();
const SkColor bg_color = color_provider->GetControlsLayerColor(
AshColorProvider::ControlsLayerType::kControlBackgroundColorActive);
const SkColor border_color = color_provider->GetControlsLayerColor(
AshColorProvider::ControlsLayerType::kFocusRingColor);
const gfx::Rect content = GetContentsBounds();
const gfx::Point center(
(content.width() + calendar_utils::kDateHorizontalPadding * 2) / 2,
(content.height() + calendar_utils::kDateVerticalPadding * 2) / 2);
if (views::View::HasFocus()) {
cc::PaintFlags highlight_border;
highlight_border.setColor(border_color);
highlight_border.setAntiAlias(true);
highlight_border.setStyle(cc::PaintFlags::kStroke_Style);
highlight_border.setStrokeWidth(kBorderLineThickness);
canvas->DrawCircle(center, kBorderRadius, highlight_border);
}
if (calendar_utils::IsToday(date_)) {
cc::PaintFlags highlight_background;
highlight_background.setColor(bg_color);
highlight_background.setStyle(cc::PaintFlags::kFill_Style);
highlight_background.setAntiAlias(true);
canvas->DrawCircle(center,
views::View::HasFocus() ? kTodayFocusedRoundedRadius
: kTodayRoundedRadius,
highlight_background);
}
}
void CalendarDateCellView::OnSelectedDateUpdated() {
const bool is_selected =
calendar_view_controller_->selected_date_cell_view() == this;
// If the selected day changes, repaint the background.
if (is_selected_ != is_selected) {
is_selected_ = is_selected;
SchedulePaint();
if (!is_selected_) {
SetAccessibleName(tool_tip_);
return;
}
// Sets accessible label. E.g. Calendar, week of July 16th 2021, [selected
// date] is currently selected.
base::Time local_date = date_ + time_difference_;
base::Time::Exploded date_exploded =
calendar_utils::GetExplodedUTC(local_date);
base::Time first_day_of_week =
date_ - base::Days(date_exploded.day_of_week);
SetAccessibleName(l10n_util::GetStringFUTF16(
IDS_ASH_CALENDAR_SELECTED_DATE_CELL_ACCESSIBLE_DESCRIPTION,
calendar_utils::GetMonthDayYearWeek(first_day_of_week),
calendar_utils::GetDayOfMonth(date_)));
}
}
void CalendarDateCellView::CloseEventList() {
if (!is_selected_)
return;
// If this date is selected, repaint the background.
is_selected_ = false;
SchedulePaint();
}
void CalendarDateCellView::EnableFocus() {
if (grayed_out_)
return;
SetFocusBehavior(FocusBehavior::ALWAYS);
}
void CalendarDateCellView::DisableFocus() {
SetFocusBehavior(FocusBehavior::NEVER);
}
void CalendarDateCellView::SetTooltipAndAccessibleName() {
std::u16string formatted_date = calendar_utils::GetMonthDayYearWeek(date_);
if (!calendar_utils::IsActiveUser()) {
tool_tip_ = formatted_date;
} else {
if (is_fetched_) {
const int tooltip_id =
event_number_ == 1 ? IDS_ASH_CALENDAR_DATE_CELL_TOOLTIP
: IDS_ASH_CALENDAR_DATE_CELL_PLURAL_EVENTS_TOOLTIP;
tool_tip_ = l10n_util::GetStringFUTF16(
tooltip_id, formatted_date,
base::UTF8ToUTF16(base::NumberToString(event_number_)));
} else {
const int tooltip_id = IDS_ASH_CALENDAR_DATE_CELL_LOADING_TOOLTIP;
tool_tip_ = l10n_util::GetStringFUTF16(tooltip_id, formatted_date);
}
}
SetTooltipText(tool_tip_);
SetAccessibleName(tool_tip_);
}
void CalendarDateCellView::UpdateFetchStatus(bool is_fetched) {
// No need to re-paint the grayed out cells, since here should be no change
// for them.
if (grayed_out_)
return;
if (!calendar_utils::IsActiveUser()) {
SetTooltipAndAccessibleName();
return;
}
// If the fetching status remains unfetched, no need to schedule repaint.
if (!is_fetched_ && !is_fetched)
return;
// If the events are fetched, gets the event number and checks if the event
// number has been changed. If the event number hasn't been changed and the
// events have been fetched before (i.e. a re-fetch with no event number
// change), no need to repaint. In all other cases, schedules a repaint.
if (is_fetched) {
const int event_number = calendar_view_controller_->GetEventNumber(date_);
if (event_number_ == event_number && is_fetched_)
return;
event_number_ = event_number;
}
is_fetched_ = is_fetched;
SetTooltipAndAccessibleName();
SchedulePaint();
}
void CalendarDateCellView::SetFirstOnFocusedAccessibilityLabel() {
SetAccessibleName(l10n_util::GetStringFUTF16(
IDS_ASH_CALENDAR_DATE_CELL_ON_FOCUS_ACCESSIBLE_DESCRIPTION, tool_tip_));
}
gfx::Point CalendarDateCellView::GetEventsPresentIndicatorCenterPosition() {
const gfx::Rect content = GetContentsBounds();
return gfx::Point(
(content.width() + calendar_utils::kDateHorizontalPadding * 2) / 2,
content.height() + calendar_utils::kDateVerticalPadding +
kGapBetweenDateAndIndicator);
}
void CalendarDateCellView::MaybeDrawEventsIndicator(gfx::Canvas* canvas) {
// Not drawing the event dot if it's a grayed out cell or the user is not in
// an active session (without a vilid user account id).
if (grayed_out_ || !calendar_utils::IsActiveUser())
return;
if (event_number_ == 0)
return;
const SkColor indicator_color =
calendar_utils::IsToday(date_)
? AshColorProvider::Get()->GetBaseLayerColor(
AshColorProvider::BaseLayerType::kTransparent90)
: AshColorProvider::Get()->GetControlsLayerColor(
AshColorProvider::ControlsLayerType::kFocusRingColor);
const float indicator_radius = is_selected_ ? kEventsPresentRoundedRadius * 2
: kEventsPresentRoundedRadius;
cc::PaintFlags indicator_paint_flags;
indicator_paint_flags.setColor(indicator_color);
indicator_paint_flags.setStyle(cc::PaintFlags::kFill_Style);
indicator_paint_flags.setAntiAlias(true);
canvas->DrawCircle(GetEventsPresentIndicatorCenterPosition(),
indicator_radius, indicator_paint_flags);
}
void CalendarDateCellView::PaintButtonContents(gfx::Canvas* canvas) {
views::LabelButton::PaintButtonContents(canvas);
if (grayed_out_)
return;
const AshColorProvider* color_provider = AshColorProvider::Get();
if (calendar_utils::IsToday(date_)) {
const SkColor text_color = color_provider->GetContentLayerColor(
AshColorProvider::ContentLayerType::kButtonLabelColorPrimary);
SetEnabledTextColors(text_color);
} else if (is_selected_) {
const SkColor text_color = color_provider->GetContentLayerColor(
AshColorProvider::ContentLayerType::kIconColorProminent);
SetEnabledTextColors(text_color);
} else {
SetEnabledTextColors(grayed_out_ ? calendar_utils::GetSecondaryTextColor()
: calendar_utils::GetPrimaryTextColor());
}
MaybeDrawEventsIndicator(canvas);
}
void CalendarDateCellView::OnDateCellActivated(const ui::Event& event) {
if (grayed_out_ || !calendar_utils::IsActiveUser())
return;
// Explicitly request focus after being activated to ensure focus moves away
// from any CalendarDateCellView which was focused prior.
RequestFocus();
calendar_metrics::RecordCalendarDateCellActivated(event);
calendar_view_controller_->ShowEventListView(/*selected_date_cell_view=*/this,
date_, row_index_);
}
CalendarMonthView::CalendarMonthView(
const base::Time first_day_of_month,
CalendarViewController* calendar_view_controller)
: calendar_view_controller_(calendar_view_controller),
calendar_model_(Shell::Get()->system_tray_model()->calendar_model()) {
views::TableLayout* layout =
SetLayoutManager(std::make_unique<views::TableLayout>());
// This layer is required for animations.
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
calendar_utils::SetUpWeekColumns(layout);
base::TimeDelta const time_difference =
calendar_utils::GetTimeDifference(first_day_of_month);
// Using the time difference to get the local `base::Time`, which is used to
// generate the exploded.
base::Time first_day_of_month_local = first_day_of_month + time_difference;
base::Time::Exploded first_day_of_month_exploded =
calendar_utils::GetExplodedUTC(first_day_of_month_local);
// Find the first day of the week.
base::Time current_date =
calendar_utils::GetFirstDayOfWeekLocalMidnight(first_day_of_month);
base::Time current_date_local = current_date + time_difference;
base::Time::Exploded current_date_exploded =
calendar_utils::GetExplodedUTC(current_date_local);
// Fetch events for the month.
fetch_month_ = first_day_of_month_local.UTCMidnight();
FetchEvents(fetch_month_);
bool is_fetched =
calendar_view_controller_->isSuccessfullyFetched(fetch_month_);
// TODO(https://crbug.com/1236276): Extract the following 3 parts (while
// loops) into a method.
int column = 0;
int safe_index = 0;
// Gray-out dates in the first row, which are from the previous month.
while (current_date_exploded.month % 12 ==
(first_day_of_month_exploded.month - 1) % 12) {
AddDateCellToLayout(current_date, column,
/*is_in_current_month=*/false, /*row_index=*/0,
/*is_fetched=*/is_fetched);
MoveToNextDay(column, current_date, current_date_local,
current_date_exploded);
++safe_index;
if (safe_index == calendar_utils::kDateInOneWeek) {
NOTREACHED()
<< "Should not render more than 7 days as the grayed out cells.";
break;
}
}
int row_number = 0;
safe_index = 0;
// Builds non-gray-out dates of the current month.
while (current_date_exploded.month == first_day_of_month_exploded.month) {
// Count a row when a new row starts.
if (column == 0 || current_date_exploded.day_of_month == 1) {
++row_number;
}
auto* cell = AddDateCellToLayout(current_date, column,
/*is_in_current_month=*/true,
/*row_index=*/row_number - 1,
/*is_fetched=*/is_fetched);
// Add the first non-grayed-out cell of the row to the `focused_cells_`.
if (column == 0 || current_date_exploded.day_of_month == 1) {
focused_cells_.push_back(cell);
}
// If this row has today, updates today's row number and replaces today to
// the last element in the `focused_cells_`.
if (calendar_utils::IsToday(current_date)) {
calendar_view_controller_->set_row_height(
cell->GetPreferredSize().height());
calendar_view_controller_->set_today_row(row_number);
focused_cells_.back() = cell;
DCHECK(calendar_view_controller_->todays_date_cell_view() == nullptr);
has_today_ = true;
calendar_view_controller_->set_todays_date_cell_view(cell);
}
MoveToNextDay(column, current_date, current_date_local,
current_date_exploded);
++safe_index;
if (safe_index == 32) {
NOTREACHED() << "Should not render more than 31 days in a month.";
break;
}
}
last_row_index_ = row_number - 1;
// To receive the fetched events.
scoped_calendar_model_observer_.Observe(calendar_model_);
// Gets the fetched status again in case the events are fetched in the middle
// of rendering date cells.
bool updated_is_fetched =
calendar_view_controller_->isSuccessfullyFetched(fetch_month_);
// If the fetching status changed, schedule repaint.
if (updated_is_fetched != is_fetched)
UpdateIsFetchedAndRepaint(updated_is_fetched);
if (calendar_utils::GetDayOfWeekInt(current_date) == 1)
return;
// Adds the first several days from the next month if the last day is not the
// end day of this week. The end date of the last row should be 6 day's away
// from the first day of this week. Adds `kDurationForAdjustingDST` hours to
// cover the case 25 hours in a day due to daylight saving.
base::Time end_of_the_last_row_local =
calendar_utils::GetFirstDayOfWeekLocalMidnight(current_date) +
base::Days(6) + calendar_utils::kDurationForAdjustingDST +
time_difference;
base::Time::Exploded end_of_row_exploded =
calendar_utils::GetExplodedUTC(end_of_the_last_row_local);
safe_index = 0;
// Gray-out dates in the last row, which are from the next month.
while (current_date_exploded.day_of_month <=
end_of_row_exploded.day_of_month) {
// Next column is generated.
AddDateCellToLayout(current_date, column,
/*is_in_current_month=*/false,
/*row_index=*/row_number,
/*is_fetched=*/is_fetched);
MoveToNextDay(column, current_date, current_date_local,
current_date_exploded);
++safe_index;
if (safe_index == calendar_utils::kDateInOneWeek) {
NOTREACHED()
<< "Should not render more than 7 days as the gray out cells.";
break;
}
}
}
CalendarMonthView::~CalendarMonthView() {
calendar_model_->CancelFetch(fetch_month_);
auto* todays_date_cell_view =
calendar_view_controller_->todays_date_cell_view();
if (todays_date_cell_view && todays_date_cell_view->parent() == this)
calendar_view_controller_->set_todays_date_cell_view(nullptr);
auto* selected_date_cell_view =
calendar_view_controller_->selected_date_cell_view();
if (selected_date_cell_view && selected_date_cell_view->parent() == this)
calendar_view_controller_->set_selected_date_cell_view(nullptr);
}
void CalendarMonthView::OnEventsFetched(
const CalendarModel::FetchingStatus status,
const base::Time start_time,
const google_apis::calendar::EventList* events) {
if (status == CalendarModel::kSuccess && start_time == fetch_month_)
UpdateIsFetchedAndRepaint(true);
}
CalendarDateCellView* CalendarMonthView::AddDateCellToLayout(
base::Time current_date,
int column,
bool is_in_current_month,
int row_index,
bool is_fetched) {
auto* layout_manager = static_cast<views::TableLayout*>(GetLayoutManager());
if (column == 0)
layout_manager->AddRows(1, views::TableLayout::kFixedSize);
return AddChildView(std::make_unique<CalendarDateCellView>(
calendar_view_controller_, current_date,
calendar_utils::GetTimeDifference(current_date),
/*is_grayed_out_date=*/!is_in_current_month, /*row_index=*/row_index,
/*is_fetched=*/is_fetched));
}
void CalendarMonthView::FetchEvents(const base::Time& month) {
calendar_model_->FetchEvents(month);
}
void CalendarMonthView::EnableFocus() {
for (auto* cell : children())
static_cast<CalendarDateCellView*>(cell)->EnableFocus();
}
void CalendarMonthView::DisableFocus() {
for (auto* cell : children())
static_cast<CalendarDateCellView*>(cell)->DisableFocus();
}
void CalendarMonthView::UpdateIsFetchedAndRepaint(bool updated_is_fetched) {
for (auto* cell : children())
static_cast<CalendarDateCellView*>(cell)->UpdateFetchStatus(
updated_is_fetched);
}
BEGIN_METADATA(CalendarDateCellView, views::View)
END_METADATA
} // namespace ash