#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/style/typography.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 "ash/system/time/date_helper.h"
#include "base/check.h"
#include "base/debug/crash_logging.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "chromeos/ash/components/settings/timezone_settings.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/compositor/layer.h"
#include "ui/events/event.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets_f.h"
#include "ui/views/accessibility/view_accessibility.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 {
constexpr int kBorderLineThickness = 2;
constexpr float kBorderRadius = 21.f;
constexpr float kBorderRadiusGlanceables = 19.f;
constexpr float kTodayBorderRadius = 100.f;
constexpr float kTodayRoundedBackgroundHorizontalInset = 8.f;
constexpr float kTodayRoundedBackgroundVerticalInset = 0.f;
constexpr float kTodayRoundedBackgroundHorizontalFocusedInset =
kTodayRoundedBackgroundHorizontalInset + kBorderLineThickness + 2.f;
constexpr float kTodayRoundedBackgroundVerticalFocusedInset =
kTodayRoundedBackgroundVerticalInset + kBorderLineThickness + 2.f;
constexpr float kTodayRoundedBackgroundHorizontalInsetGlanceables = 9.f;
constexpr float kEventsPresentRoundedRadius = 1.f;
constexpr int kGapBetweenDateAndIndicator = 1;
constexpr int kDateCellVerticalPaddingGlanceables = 10;
constexpr auto kDateCellInsetsGlanceables =
gfx::Insets::VH(kDateCellVerticalPaddingGlanceables, 16);
void MoveToNextDay(int& column,
base::Time& current_date,
base::Time& local_current_date,
base::Time::Exploded& current_date_exploded) {
int hours = current_date_exploded.hour;
current_date += base::Hours(30 - hours);
local_current_date += base::Hours(30 - hours);
local_current_date.UTCExplode(¤t_date_exploded);
column = (column + 1) % calendar_utils::kDateInOneWeek;
}
}
CalendarDateCellView::CalendarDateCellView(
CalendarViewController* calendar_view_controller,
base::Time date,
base::TimeDelta time_difference,
bool is_grayed_out_date,
bool should_fetch_calendar_data,
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),
should_fetch_calendar_data_(should_fetch_calendar_data),
row_index_(row_index),
is_fetched_(is_fetched),
is_today_(calendar_utils::IsToday(date)),
time_difference_(time_difference),
calendar_view_controller_(calendar_view_controller) {
SetHorizontalAlignment(gfx::ALIGN_CENTER);
SetBorder(views::CreateEmptyBorder(
features::AreAnyGlanceablesTimeManagementViewsEnabled()
? kDateCellInsetsGlanceables
: calendar_utils::kDateCellInsets));
label()->SetElideBehavior(gfx::NO_ELIDE);
label()->SetSubpixelRenderingEnabled(false);
if (is_today_) {
TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosButton1,
*label());
} else {
TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosBody1,
*label());
}
views::FocusRing::Remove(this);
DisableFocus();
if (!grayed_out_) {
if (should_fetch_calendar_data_ && is_fetched_) {
UpdateFetchStatus(true);
}
SetTooltipAndAccessibleName();
is_selected_ = calendar_view_controller->selected_date_cell_view() == this;
}
scoped_calendar_view_controller_observer_.Observe(
calendar_view_controller_.get());
}
CalendarDateCellView::~CalendarDateCellView() = default;
void CalendarDateCellView::OnThemeChanged() {
views::View::OnThemeChanged();
SetEnabledTextColors(grayed_out_ ? cros_tokens::kCrosSysOnSurfaceVariant
: cros_tokens::kCrosSysOnSurface);
}
void CalendarDateCellView::OnPaintBackground(gfx::Canvas* canvas) {
if (grayed_out_) {
return;
}
const gfx::Rect content = GetContentsBounds();
const gfx::SizeF local_bounds = gfx::SizeF(GetLocalBounds().size());
const SkColor border_color =
GetColorProvider()->GetColor(cros_tokens::kCrosSysFocusRing);
cc::PaintFlags highlight_border;
highlight_border.setColor(border_color);
highlight_border.setAntiAlias(true);
highlight_border.setStyle(cc::PaintFlags::kStroke_Style);
highlight_border.setStrokeWidth(kBorderLineThickness);
const bool is_for_glanceables =
features::AreAnyGlanceablesTimeManagementViewsEnabled();
if (is_today_) {
gfx::RectF background_rect(local_bounds);
const SkColor bg_color = GetColorProvider()->GetColor(
cros_tokens::kCrosSysSystemPrimaryContainer);
cc::PaintFlags highlight_background;
highlight_background.setColor(bg_color);
highlight_background.setStyle(cc::PaintFlags::kFill_Style);
highlight_background.setAntiAlias(true);
if (views::View::HasFocus()) {
gfx::RectF border_rect(local_bounds);
const int half_stroke_thickness = kBorderLineThickness / 2;
border_rect.Inset(gfx::InsetsF::VH(
half_stroke_thickness,
is_for_glanceables ? kTodayRoundedBackgroundHorizontalInsetGlanceables
: kTodayRoundedBackgroundHorizontalInset));
canvas->DrawRoundRect(border_rect, kTodayBorderRadius, highlight_border);
background_rect.Inset(
gfx::InsetsF::VH(kTodayRoundedBackgroundVerticalFocusedInset,
kTodayRoundedBackgroundHorizontalFocusedInset));
canvas->DrawRoundRect(background_rect, kTodayBorderRadius,
highlight_background);
return;
}
background_rect.Inset(
gfx::InsetsF::VH(kTodayRoundedBackgroundVerticalInset,
kTodayRoundedBackgroundHorizontalInset));
canvas->DrawRoundRect(background_rect, kTodayBorderRadius,
highlight_background);
return;
}
if (views::View::HasFocus() || is_selected_) {
const gfx::Point center(
(content.width() + calendar_utils::kDateHorizontalPadding * 2) / 2,
(content.height() + (is_for_glanceables
? kDateCellVerticalPaddingGlanceables
: calendar_utils::kDateVerticalPadding) *
2) /
2);
canvas->DrawCircle(
center, is_for_glanceables ? kBorderRadiusGlanceables : kBorderRadius,
highlight_border);
}
}
void CalendarDateCellView::OnSelectedDateUpdated() {
const bool is_selected =
calendar_view_controller_->selected_date_cell_view() == this;
if (is_selected_ != is_selected) {
is_selected_ = is_selected;
SchedulePaint();
if (!is_selected_) {
GetViewAccessibility().SetName(tool_tip_);
return;
}
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);
GetViewAccessibility().SetName(l10n_util::GetStringFUTF16(
IDS_ASH_CALENDAR_SELECTED_DATE_CELL_ACCESSIBLE_DESCRIPTION,
calendar_utils::GetMonthDayYear(first_day_of_week),
calendar_utils::GetDayOfMonth(date_)));
}
}
void CalendarDateCellView::CloseEventList() {
if (!is_selected_) {
return;
}
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 (!should_fetch_calendar_data_) {
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_);
GetViewAccessibility().SetName(tool_tip_);
}
void CalendarDateCellView::UpdateFetchStatus(bool is_fetched) {
if (grayed_out_) {
return;
}
if (!should_fetch_calendar_data_) {
SetTooltipAndAccessibleName();
return;
}
if (!is_fetched_ && !is_fetched) {
return;
}
if (is_fetched) {
const int event_number = calendar_view_controller_->GetEventNumber(date_);
if (event_number_ == event_number && is_fetched_) {
return;
}
event_number_ = event_number;
if (is_today_) {
calendar_view_controller_->OnTodaysEventFetchComplete();
}
}
is_fetched_ = is_fetched;
SetTooltipAndAccessibleName();
SchedulePaint();
}
void CalendarDateCellView::SetFirstOnFocusedAccessibilityLabel() {
GetViewAccessibility().SetName(l10n_util::GetStringFUTF16(
IDS_ASH_CALENDAR_DATE_CELL_ON_FOCUS_ACCESSIBLE_DESCRIPTION, tool_tip_));
}
void CalendarDateCellView::PaintButtonContents(gfx::Canvas* canvas) {
views::LabelButton::PaintButtonContents(canvas);
if (grayed_out_) {
return;
}
SetEnabledTextColors(is_today_ ? cros_tokens::kCrosSysSystemOnPrimaryContainer
: cros_tokens::kCrosSysOnSurface);
MaybeDrawEventsIndicator(canvas);
}
void CalendarDateCellView::OnDateCellActivated(const ui::Event& event) {
if (grayed_out_ || !should_fetch_calendar_data_ ||
!calendar_view_controller_->is_date_cell_clickable()) {
return;
}
RequestFocus();
calendar_metrics::RecordCalendarDateCellActivated(event);
calendar_view_controller_->ShowEventListView(this,
date_, row_index_);
}
gfx::Point CalendarDateCellView::GetEventsPresentIndicatorCenterPosition() {
const gfx::Rect content = GetContentsBounds();
const int horizontal_padding = calendar_utils::kDateHorizontalPadding;
const int vertical_padding =
features::AreAnyGlanceablesTimeManagementViewsEnabled()
? kDateCellVerticalPaddingGlanceables
: calendar_utils::kDateVerticalPadding;
return gfx::Point(
(content.width() + horizontal_padding * 2) / 2,
content.height() + vertical_padding + kGapBetweenDateAndIndicator);
}
void CalendarDateCellView::MaybeDrawEventsIndicator(gfx::Canvas* canvas) {
if (grayed_out_ || !should_fetch_calendar_data_) {
return;
}
if (event_number_ == 0) {
return;
}
const auto* color_provider = GetColorProvider();
const SkColor indicator_color = color_provider->GetColor(
is_today_ ? cros_tokens::kCrosSysSystemOnPrimaryContainer
: cros_tokens::kCrosSysOnSurface);
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);
is_events_indicator_drawn = true;
}
CalendarMonthView::CalendarMonthView(
const base::Time first_day_of_month,
CalendarViewController* calendar_view_controller)
: calendar_view_controller_(calendar_view_controller),
calendar_list_model_(
Shell::Get()->system_tray_model()->calendar_list_model()),
calendar_model_(Shell::Get()->system_tray_model()->calendar_model()) {
views::TableLayout* layout =
SetLayoutManager(std::make_unique<views::TableLayout>());
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
calendar_utils::SetUpWeekColumns(layout);
base::TimeDelta const time_difference =
calendar_utils::GetTimeDifference(first_day_of_month);
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);
base::Time current_date =
calendar_utils::GetFirstDayOfWeekLocalMidnight(first_day_of_month) +
base::Hours(8);
base::Time current_date_local = current_date + time_difference;
base::Time::Exploded current_date_exploded =
calendar_utils::GetExplodedUTC(current_date_local);
fetch_month_ = first_day_of_month_local.UTCMidnight();
if (calendar_utils::IsMultiCalendarEnabled()) {
scoped_calendar_list_model_observer_.Observe(calendar_list_model_.get());
calendar_model_->MaybeFetchEvents(fetch_month_);
} else {
FetchEvents(fetch_month_);
}
bool has_fetched_data =
calendar_view_controller_->IsSuccessfullyFetched(fetch_month_);
const bool should_fetch_calendar_data =
calendar_utils::ShouldFetchCalendarData();
int column = 0;
int safe_index = 0;
while (current_date_exploded.month % 12 ==
(first_day_of_month_exploded.month - 1) % 12) {
AddDateCellToLayout(current_date, column,
false, 0,
has_fetched_data,
should_fetch_calendar_data);
MoveToNextDay(column, current_date, current_date_local,
current_date_exploded);
++safe_index;
if (safe_index == calendar_utils::kDateInOneWeek) {
DUMP_WILL_BE_NOTREACHED()
<< "Should not render more than 7 days as the grayed out cells.";
break;
}
}
int row_number = 0;
safe_index = 0;
while (current_date_exploded.month == first_day_of_month_exploded.month) {
if (column == 0 || current_date_exploded.day_of_month == 1) {
++row_number;
}
auto* cell = AddDateCellToLayout(current_date, column,
true,
row_number - 1,
has_fetched_data,
should_fetch_calendar_data);
if (column == 0 || current_date_exploded.day_of_month == 1) {
focused_cells_.push_back(cell);
}
if (cell->is_today()) {
calendar_view_controller_->set_row_height(
cell->GetPreferredSize().height());
calendar_view_controller_->set_today_row(row_number);
focused_cells_.back() = cell;
has_today_ = true;
DCHECK(calendar_view_controller_->todays_date_cell_view() == nullptr);
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.";
}
}
last_row_index_ = row_number - 1;
scoped_calendar_model_observer_.Observe(calendar_model_.get());
bool updated_has_fetched_data =
calendar_view_controller_->IsSuccessfullyFetched(fetch_month_);
if (updated_has_fetched_data != has_fetched_data) {
UpdateIsFetchedAndRepaint(updated_has_fetched_data);
}
auto gray_out_days = 8 - calendar_utils::GetDayOfWeekInt(current_date);
if (gray_out_days == 7) {
return;
}
for (int rendered_days = 0; rendered_days < gray_out_days; rendered_days++) {
AddDateCellToLayout(current_date, column,
false,
row_number,
has_fetched_data,
should_fetch_calendar_data);
MoveToNextDay(column, current_date, current_date_local,
current_date_exploded);
}
}
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::OnCalendarListFetchComplete() {
if (calendar_utils::IsMultiCalendarEnabled()) {
calendar_model_->FetchEvents(fetch_month_);
}
}
void CalendarMonthView::OnEventsFetched(
const CalendarModel::FetchingStatus status,
const base::Time start_time) {
if (status == CalendarModel::kSuccess && start_time == fetch_month_) {
UpdateIsFetchedAndRepaint(true);
}
if (!(calendar_model_->MonthHasEvents(start_time))) {
return;
}
has_events_ = true;
if (start_time ==
calendar_view_controller_->GetOnScreenMonthFirstDayUTC().UTCMidnight()) {
calendar_view_controller_->EventsDisplayedToUser();
}
}
void CalendarMonthView::EnableFocus() {
for (views::View* cell : children()) {
static_cast<CalendarDateCellView*>(cell)->EnableFocus();
}
}
void CalendarMonthView::DisableFocus() {
for (views::View* cell : children()) {
static_cast<CalendarDateCellView*>(cell)->DisableFocus();
}
}
void CalendarMonthView::UpdateIsFetchedAndRepaint(bool updated_is_fetched) {
for (views::View* cell : children()) {
static_cast<CalendarDateCellView*>(cell)->UpdateFetchStatus(
updated_is_fetched);
}
}
BEGIN_METADATA(CalendarMonthView)
END_METADATA
CalendarDateCellView* CalendarMonthView::AddDateCellToLayout(
base::Time current_date,
int column,
bool is_in_current_month,
int row_index,
bool is_fetched,
bool should_fetch_calendar_data) {
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_in_current_month, should_fetch_calendar_data,
row_index,
is_fetched));
}
void CalendarMonthView::FetchEvents(const base::Time& month) {
calendar_model_->FetchEvents(month);
}
BEGIN_METADATA(CalendarDateCellView)
END_METADATA
}