chromium/ash/system/time/calendar_view.h

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef ASH_SYSTEM_TIME_CALENDAR_VIEW_H_
#define ASH_SYSTEM_TIME_CALENDAR_VIEW_H_

#include <string>

#include "ash/ash_export.h"
#include "ash/shell.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/time/calendar_list_model.h"
#include "ash/system/time/calendar_model.h"
#include "ash/system/time/calendar_up_next_view.h"
#include "ash/system/time/calendar_view_controller.h"
#include "ash/system/unified/glanceable_tray_child_bubble.h"
#include "ash/system/unified/unified_system_tray_controller.h"
#include "base/callback_list.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/scoped_multi_source_observation.h"
#include "base/scoped_observation.h"
#include "base/timer/timer.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/view.h"

namespace ui {
class Event;
}  // namespace ui

namespace views {

class Label;

}  // namespace views

namespace ash {

class CalendarEventListView;
class CalendarMonthView;
class GlanceablesProgressBarView;
class IconButton;
class TriView;

// The header of the calendar view, which shows the current month and year.
class CalendarHeaderView : public views::View {
  METADATA_HEADER(CalendarHeaderView, views::View)

 public:
  CalendarHeaderView(const std::u16string& month, const std::u16string& year);
  CalendarHeaderView(const CalendarHeaderView& other) = delete;
  CalendarHeaderView& operator=(const CalendarHeaderView& other) = delete;
  ~CalendarHeaderView() override;

  // Updates the month and year labels.
  void UpdateHeaders(const std::u16string& month, const std::u16string& year);

 private:
  friend class CalendarViewAnimationTest;
  friend class CalendarViewTest;

  // The main header which shows the month name.
  const raw_ptr<views::Label> header_;

  // The year header which follows the `header_`.
  const raw_ptr<views::Label> header_year_;
};

// This view displays a scrollable calendar.
class ASH_EXPORT CalendarView : public CalendarModel::Observer,
                                public CalendarViewController::Observer,
                                public GlanceableTrayChildBubble,
                                public views::ViewObserver {
  METADATA_HEADER(CalendarView, GlanceableTrayChildBubble)

 public:
  // `use_glanceables_container_style` - Whether the calendar view is shown as a
  // bubble in glanceables container, or a `UnifiedSystemTrayBubble` (which is
  // the case if glanceables feature is not enabled).
  explicit CalendarView(bool use_glanceables_container_style);
  CalendarView(const CalendarView& other) = delete;
  CalendarView& operator=(const CalendarView& other) = delete;
  ~CalendarView() override;

  // CalendarModel::Observer:
  void OnEventsFetched(const CalendarModel::FetchingStatus status,
                       const base::Time start_time) override;

  // CalendarViewController::Observer:
  void OnMonthChanged() override;
  void OpenEventList() override;
  void CloseEventList() override;
  void OnSelectedDateUpdated() override;
  void OnCalendarLoaded() override;

  // views::ViewObserver:
  void OnViewBoundsChanged(views::View* observed_view) override;
  void OnViewFocused(View* observed_view) override;

  // views::View:
  void OnEvent(ui::Event* event) override;

  CalendarViewController* calendar_view_controller() {
    return calendar_view_controller_.get();
  }

  CalendarUpNextView* up_next_view() { return up_next_view_; }
  CalendarEventListView* event_list_view() { return event_list_view_; }

  enum class CalendarSlidingSurfaceBoundsType {
    // The bounds should be `event_list_view_`'s bounds. This is used when the
    // `event_list_view_` is showing.
    EVENT_LIST_VIEW_BOUNDS,

    // The bounds should be `up_next_view_`'s bounds. This is used mostly when
    // the `up_next_view_` is showing.
    UP_NEXT_VIEW_BOUNDS,

    // The bounds should be at the bottom of the calendar bubble. This is used
    // when neither the `event_list_view_` or the `up_next_view_` is showing.
    CALENDAR_BOTTOM_BOUNDS
  };

  // Sets the bounds of the container of the `up_next_view_` and
  // `event_list_view_` to be flush with the bottom of the scroll view. Only the
  // position will be animated, so give the view its final bounds.
  // `CalendarSlidingSurfaceBoundsType` needs to be passed in to determine the
  // state of the bounds.
  void SetCalendarSlidingSurfaceBounds(CalendarSlidingSurfaceBoundsType type);

 private:
  // The header of each month view which shows the month's name. If the year of
  // this month is not the same as the current month, the year is also shown in
  // this view.
  class MonthHeaderLabelView;

  // Content view of calendar's scroll view, used for metrics recording.
  // TODO(crbug.com/1297376): Add unit tests for metrics recording.
  class ScrollContentsView : public views::View {
    METADATA_HEADER(ScrollContentsView, views::View)

   public:
    explicit ScrollContentsView(CalendarViewController* controller);
    ScrollContentsView(const ScrollContentsView& other) = delete;
    ScrollContentsView& operator=(const ScrollContentsView& other) = delete;
    ~ScrollContentsView() override = default;

    // Update the value of current month based on the controller.
    void OnMonthChanged();

    // views::View:
    void OnEvent(ui::Event* event) override;

    // Called when a stylus touch event is triggered.
    void OnStylusEvent(const ui::TouchEvent& event);

   private:
    // Used as a Shell pre-target handler to notify the owner of stylus events.
    class StylusEventHandler : public ui::EventHandler {
     public:
      explicit StylusEventHandler(ScrollContentsView* content_view);
      StylusEventHandler(const StylusEventHandler&) = delete;
      StylusEventHandler& operator=(const StylusEventHandler&) = delete;
      ~StylusEventHandler() override;

      // ui::EventHandler:
      void OnTouchEvent(ui::TouchEvent* event) override;

     private:
      raw_ptr<ScrollContentsView> content_view_;
    };

    const raw_ptr<CalendarViewController, DanglingUntriaged> controller_;
    StylusEventHandler stylus_event_handler_;

    // Since we only record metrics once when we scroll through a particular
    // month. This keeps track the current month in display that we have already
    // recorded metrics.
    std::u16string current_month_;
  };

  // The types to create the `MonthHeaderLabelView` which are in corresponding
  // to the 4 months: `previous_month_`, `current_month_`, `next_month_` and
  // `next_next_month_`.
  enum LabelType { PREVIOUS, CURRENT, NEXT, NEXTNEXT };

  friend class CalendarViewTest;
  friend class CalendarViewPixelTest;
  friend class CalendarViewAnimationTest;

  // Creates the new header of the calendar view, which includes a
  // `CalendarHeaderView`, a reset to today button, and up/down buttons.
  views::View* CreateCalendarHeaderRow();

  // Creates the calendar view title that includes a label,
  // `reset_to_today_button_`, and a `settings_button_`.
  void CreateCalendarTitleRow();

  // Creates the `CalendarHeaderView`s container that contains `header_` and
  // `temp_header_`.
  views::View* CreateMonthHeaderContainer();

  // Creates the button container that contains `up_button_` and `down_button_`.
  views::View* CreateButtonContainer();

  // Assigns month views and labels based on the current date on screen.
  void SetMonthViews();

  // Returns the current month first row position.
  int GetPositionOfCurrentMonth() const;

  // Returns the today's row position.
  int GetPositionOfToday() const;

  // Returns the selected date's row position.
  int GetPositionOfSelectedDate() const;

  // Returns the calculated height of a single visible row.
  int GetSingleVisibleRowHeight() const;

  // Adds a month label.
  views::View* AddLabelWithId(LabelType type, bool add_at_front = false);

  // Adds a `CalendarMonthView`.
  CalendarMonthView* AddMonth(base::Time month_first_date,
                              bool add_at_front = false);

  // Deletes the current next month and add a new month at the top of the
  // `content_view_`.
  void ScrollUpOneMonth();

  // Deletes the current previous month and adds a new month at the bottom of
  // the `content_view_`.
  void ScrollDownOneMonth();

  // Scrolls up or down one month then auto scrolls to the current month's first
  // row.
  void ScrollOneMonthAndAutoScroll(bool scroll_up);

  // Shows the scrolling animation then scrolls one month then auto scroll to
  // the current month's first row.
  void ScrollOneMonthWithAnimation(bool scroll_up);

  // Sets up the `temp_header_` to prepare for the header animation and returns
  // the moving transform for the header.
  gfx::Transform GetHeaderMovingAndPrepareAnimation(
      bool scroll_up,
      const std::string& animation_name,
      const std::u16string& temp_month,
      const std::u16string& temp_year);

  // Scrolls up/down one row based on `scroll_up`.
  void ScrollOneRowWithAnimation(bool scroll_up);

  // Sets opacity for header and content view (which contains previous, current
  // and next month with their labels),
  void SetHeaderAndContentViewOpacity(float opacity);

  // Enables or disables `should_months_animate_` and `scroll_view_` vertical
  // scroll bar mode.
  void SetShouldMonthsAnimateAndScrollEnabled(bool enabled);

  // Fades out on-screen month, sets date to today by calling `ResetToToday` and
  // fades in updated views after.
  void ResetToTodayWithAnimation();

  // Removes on-screen month and adds today's date month and label views without
  // animation.
  void ResetToToday();

  // Updates the on-screen month map with the current months on screen.
  void UpdateOnScreenMonthMap();

  // Returns true if there is no Calendar List fetch in progress.
  bool CalendarsFetchComplete();

  // Returns whether or not we've finished fetching CalendarEvents.
  bool EventsFetchComplete();

  // Creates and adds the `up_next_view_` if it's not created yet.
  void MaybeCreateUpNextView();

  // Checks if all months in the visible window have finished fetching. If so,
  // stop showing the loading bar.
  void MaybeUpdateLoadingBarVisibility();

  // Fades in current month.
  void FadeInCurrentMonth();

  // Updates the `header_`'s month and year to the current month and year.
  void UpdateHeaders();

  // Resets the `header_`'s opacity and position. Also resets
  // `scrolling_settled_timer_` and `header_animation_restart_timer_`.
  void RestoreHeadersStatus();

  // Resets the the month views' opacity and position. In case the animation is
  // aborted in the middle and the view's are not in the original status.
  void RestoreMonthStatus();

  // Auto scrolls to today. If the view is big enough we scroll to the first row
  // of today's month, otherwise we scroll to the position of today's row.
  void ScrollToToday();

  // If currently focusing on any date cell.
  bool IsDateCellViewFocused();

  // Returns whether `header_`, `current_month_`, `content_view_`, or
  // `event_list_view_` are animating.
  bool IsAnimating();

  // If focusing on `CalendarDateCellView` is interrupted (by scrolling or by
  // today's button), resets the content view's `FocusBehavior` to `ALWAYS`.
  void MaybeResetContentViewFocusBehavior();

  // We only fetch events after we've "settled" on the current on-screen month.
  void OnScrollingSettledTimerFired();

  // Sets `expanded_row_index_` and auto-scrolls the `scroll_view_` when
  // `event_list_view_` is opened. After scrolling, disables the scroll bar.
  void SetExpandedRowThenDisableScroll(int row_index);

  // ScrollView callback.
  void OnContentsScrolled();

  // Callback passed to `up_button_` and `down_button_`, activated on button
  // activation.
  void OnMonthArrowButtonActivated(bool up, const ui::Event& event);

  // Adjusts the Chrome Vox box position for date cells in the scroll view.
  void AdjustDateCellVoxBounds();

  // Performs cleanup on temporary views after the scroll animation is complete,
  // and re-enables the month and header animation.
  void OnScrollMonthAnimationComplete(bool scroll_up);

  // Handles the position and status of `event_list_view_` and other views after
  // the opening event list animation or closing event list animation. Such as
  // restoring the position of them, re-enabling animation and etc.
  void OnOpenEventListAnimationComplete();
  void OnCloseEventListAnimationComplete();

  // Requests the focusing ring to go to the close button of `event_list_view_`.
  void RequestFocusForEventListCloseButton();

  // Animates the month and scrolls back to today and resets the
  // `scrolling_settled_timer_` to update the `on_screen_month_` map after the
  // resetting to today animation.
  void OnResetToTodayAnimationComplete();

  // Enables the month and header animation, restores the header and content
  // opacity.
  void OnResetToTodayFadeInAnimationComplete();

  // Tries to focus the preferred CalendarDateCellView. If `prefer_today` is
  // true, preferred CalendarDateCellView is todays CalendarDateCellView,
  // otherwise preferred view is the selected CalendarDateCellView. If the
  // preferred view is not visible, focus the first visible view.
  void FocusPreferredDateCellViewOrFirstVisible(bool prefer_today);

  // Returns `target_date_cell_view` if it is in the visible window of
  // `scroll_view_` and in `current_month_`. Otherwise returns the first visible
  // focusable date cell on the first fully visible row.
  CalendarDateCellView* GetTargetDateCellViewOrFirstFocusable(
      CalendarDateCellView* target_date_cell_view);

  // Calculates the first fully visible row (which lives in `content_view_`)
  // shown in `scroll_view_`'s visible window.
  int CalculateFirstFullyVisibleRow();

  // Conditionally displays the `up_next_view_`.
  void MaybeShowUpNextView();

  // Removes the `up_next_view_`.
  void RemoveUpNextView();

  // Used by the `CalendarUpNextView` to open the event list for today's date.
  void OpenEventListForTodaysDate();

  enum class ScrollViewState {
    FULL_HEIGHT,
    UP_NEXT_SHOWING,
    EVENT_LIST_SHOWING
  };
  // Used for clipping the calendar scroll view height to the different states
  // that the calendar view can be in.
  void ClipScrollViewHeight(ScrollViewState state_to_change_to);

  // Fades in or out the `up_next_view_`.
  void FadeInUpNextView();
  void FadeOutUpNextView();

  // Callback after the `FadeInUpNextView()`/`FadeOutUpNextView()` animation has
  // ended.
  void OnFadeInUpNextViewAnimationEnded();
  void OnFadeOutUpNextViewAnimationEnded();

  // Stops the `check_upcoming_events_timer_` if it's running. This is used when
  // `up_next_view_` fades out, the timer needs to stop as well.
  void StopUpNextTimer();

  // Checks if `up_next_view_` exists and is visible.
  bool IsUpNextViewVisible() const;

  // Setters for animation flags.
  void set_should_header_animate(bool should_animate) {
    should_header_animate_ = should_animate;
  }
  void set_should_months_animate(bool should_animate) {
    should_months_animate_ = should_animate;
  }

  std::unique_ptr<CalendarViewController> calendar_view_controller_;

  // Reset `scrolling_settled_timer_`.
  void reset_scrolling_settled_timer() { scrolling_settled_timer_.Reset(); }

  // The content of the `scroll_view_`, which carries months and month labels.
  // Owned by `CalendarView`.
  raw_ptr<ScrollContentsView> content_view_ = nullptr;

  // The container view for the top-most title row. Owned by `CalendarView`.
  raw_ptr<TriView> tri_view_ = nullptr;

  // The following is owned by `CalendarView`.
  raw_ptr<views::ScrollView> scroll_view_ = nullptr;
  raw_ptr<views::View, DanglingUntriaged> current_label_ = nullptr;
  raw_ptr<views::View, DanglingUntriaged> previous_label_ = nullptr;
  raw_ptr<views::View, DanglingUntriaged> next_label_ = nullptr;
  raw_ptr<views::View, DanglingUntriaged> next_next_label_ = nullptr;
  raw_ptr<CalendarMonthView, DanglingUntriaged> previous_month_ = nullptr;
  raw_ptr<CalendarMonthView, DanglingUntriaged> current_month_ = nullptr;
  raw_ptr<CalendarMonthView, DanglingUntriaged> next_month_ = nullptr;
  raw_ptr<CalendarMonthView, DanglingUntriaged> next_next_month_ = nullptr;
  raw_ptr<CalendarHeaderView> header_ = nullptr;
  // Temporary header, used for animations.
  raw_ptr<CalendarHeaderView> temp_header_ = nullptr;
  raw_ptr<views::Button> reset_to_today_button_ = nullptr;
  raw_ptr<views::Button> settings_button_ = nullptr;
  raw_ptr<IconButton> managed_button_ = nullptr;
  raw_ptr<IconButton> up_button_ = nullptr;
  raw_ptr<IconButton> down_button_ = nullptr;
  raw_ptr<GlanceablesProgressBarView> progress_bar_ = nullptr;
  raw_ptr<views::View> calendar_sliding_surface_ = nullptr;
  raw_ptr<CalendarEventListView, DanglingUntriaged> event_list_view_ = nullptr;
  // Owned by CalendarView.
  raw_ptr<CalendarUpNextView, DanglingUntriaged> up_next_view_ = nullptr;
  std::map<base::Time, CalendarModel::FetchingStatus> on_screen_month_;
  raw_ptr<CalendarListModel> calendar_list_model_ =
      Shell::Get()->system_tray_model()->calendar_list_model();
  raw_ptr<CalendarModel> calendar_model_ =
      Shell::Get()->system_tray_model()->calendar_model();

  // If it `is_resetting_scroll_`, we don't calculate the scroll position and we
  // don't need to check if we need to update the month or not.
  bool is_resetting_scroll_ = false;

  // It's true if the header should animate, but false when it is currently
  // animating, or header changing from mouse scroll (not from the buttons) or
  // cooling down from the last animation.
  bool should_header_animate_ = true;

  // It's true if the month views should animate, but false when it is currently
  // animating, or cooling down from the last animation.
  bool should_months_animate_ = true;

  // This is used to define the animation directions for updating the header and
  // month views.
  bool is_scrolling_up_ = true;

  // Whether the Calendar View is scrolling.
  bool is_calendar_view_scrolling_ = false;

  // If the Calendar View destructor is being called.
  bool is_destroying_ = false;

  // Set to true if the user has scrolled the Calendar at all, either via the
  // scroll view directly or used the month arrow buttons, in the lifetime of
  // the CalendarView.
  bool user_has_scrolled_ = false;

  // Timer that fires when the calendar view is settled on, i.e. finished
  // scrolling to, a currently-visible month
  base::RetainingOneShotTimer scrolling_settled_timer_;

  // Timers that enable the updating month/header animations. When the month
  // keeps getting changed, the animation will be disabled and the cool-down
  // duration is `kAnimationDisablingTimeout` ms to enable the next animation.
  base::RetainingOneShotTimer header_animation_restart_timer_;
  base::RetainingOneShotTimer months_animation_restart_timer_;

  // Timer that checks upcoming events periodically.
  base::RepeatingTimer check_upcoming_events_timer_;

  base::CallbackListSubscription on_contents_scrolled_subscription_;
  base::ScopedObservation<CalendarModel, CalendarModel::Observer>
      scoped_calendar_model_observer_{this};
  base::ScopedObservation<CalendarViewController,
                          CalendarViewController::Observer>
      scoped_calendar_view_controller_observer_{this};
  base::ScopedMultiSourceObservation<views::View, views::ViewObserver>
      scoped_view_observer_{this};

  base::WeakPtrFactory<CalendarView> weak_factory_{this};
};

}  // namespace ash

#endif  // ASH_SYSTEM_TIME_CALENDAR_VIEW_H_