| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/system/unified/glanceable_tray_bubble_view.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <numeric> |
| |
| #include "ash/api/tasks/tasks_client.h" |
| #include "ash/api/tasks/tasks_types.h" |
| #include "ash/constants/ash_features.h" |
| #include "ash/constants/ash_pref_names.h" |
| #include "ash/glanceables/classroom/glanceables_classroom_client.h" |
| #include "ash/glanceables/classroom/glanceables_classroom_student_view.h" |
| #include "ash/glanceables/common/glanceables_time_management_bubble_view.h" |
| #include "ash/glanceables/glanceables_controller.h" |
| #include "ash/glanceables/tasks/glanceables_tasks_view.h" |
| #include "ash/public/cpp/session/user_info.h" |
| #include "ash/public/cpp/style/color_provider.h" |
| #include "ash/session/session_controller_impl.h" |
| #include "ash/shelf/shelf.h" |
| #include "ash/shell.h" |
| #include "ash/system/time/calendar_view.h" |
| #include "ash/system/tray/tray_bubble_view.h" |
| #include "ash/system/tray/tray_constants.h" |
| #include "ash/system/tray/tray_utils.h" |
| #include "base/check.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/time/time.h" |
| #include "base/types/cxx23_to_underlying.h" |
| #include "chromeos/constants/chromeos_features.h" |
| #include "components/prefs/pref_registry_simple.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/session_manager/session_manager_types.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/base/models/list_model.h" |
| #include "ui/chromeos/styles/cros_tokens_color_mappings.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/gfx/geometry/insets.h" |
| #include "ui/views/focus/focus_manager.h" |
| #include "ui/views/highlight_border.h" |
| #include "ui/views/layout/layout_types.h" |
| |
| namespace ash { |
| |
| using BoundsType = CalendarView::CalendarSlidingSurfaceBoundsType; |
| using GlanceablesContext = GlanceablesTimeManagementBubbleView::Context; |
| |
| namespace { |
| |
| // If display height is greater than `kDisplayHeightThreshold`, the height of |
| // the `calendar_view_` is `kCalendarBubbleHeightLargeDisplay`, otherwise |
| // is `kCalendarBubbleHeightSmallDisplay`. |
| constexpr int kDisplayHeightThreshold = 800; |
| constexpr int kCalendarBubbleHeightSmallDisplay = 340; |
| constexpr int kCalendarBubbleHeightLargeDisplay = 368; |
| |
| // Tasks Glanceables constants. |
| constexpr int kGlanceablesContainerCornerRadius = 24; |
| |
| // The margin between each glanceable views. |
| constexpr int kMarginBetweenGlanceables = 8; |
| |
| void SetLastExpandedGlanceables(GlanceablesContext context) { |
| Shell::Get()->session_controller()->GetActivePrefService()->SetInteger( |
| prefs::kGlanceablesTimeManagementLastExpandedBubble, |
| base::to_underlying(context)); |
| } |
| |
| GlanceablesContext GetLastExpandedGlanceables() { |
| return static_cast<GlanceablesContext>( |
| Shell::Get()->session_controller()->GetActivePrefService()->GetInteger( |
| prefs::kGlanceablesTimeManagementLastExpandedBubble)); |
| } |
| |
| // The container view of time management glanceables, which includes Tasks and |
| // Classroom. |
| class TimeManagementContainer : public views::FlexLayoutView { |
| METADATA_HEADER(TimeManagementContainer, views::FlexLayoutView) |
| |
| public: |
| TimeManagementContainer() { |
| SetPaintToLayer(); |
| if (chromeos::features::IsSystemBlurEnabled()) { |
| layer()->SetFillsBoundsOpaquely(false); |
| layer()->SetBackgroundBlur(ColorProvider::kBackgroundBlurSigma); |
| layer()->SetBackdropFilterQuality(ColorProvider::kBackgroundBlurQuality); |
| } |
| |
| layer()->SetRoundedCornerRadius( |
| gfx::RoundedCornersF(kGlanceablesContainerCornerRadius)); |
| SetOrientation(views::LayoutOrientation::kVertical); |
| |
| // Set all inner margins and the spacing between children to 8. |
| SetInteriorMargin(gfx::Insets(8)); |
| SetCollapseMargins(true); |
| SetDefault(views::kMarginsKey, gfx::Insets::VH(8, 0)); |
| |
| const ui::ColorId background_color_id = |
| chromeos::features::IsSystemBlurEnabled() |
| ? cros_tokens::kCrosSysSystemBaseElevated |
| : cros_tokens::kCrosSysSystemBaseElevatedOpaque; |
| SetBackground(views::CreateSolidBackground(background_color_id)); |
| SetBorder(std::make_unique<views::HighlightBorder>( |
| kGlanceablesContainerCornerRadius, |
| views::HighlightBorder::Type::kHighlightBorderOnShadow)); |
| SetDefault( |
| views::kFlexBehaviorKey, |
| views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum, |
| views::MaximumFlexSizeRule::kPreferred)); |
| } |
| TimeManagementContainer(const TimeManagementContainer&) = delete; |
| TimeManagementContainer& operator=(const TimeManagementContainer&) = delete; |
| ~TimeManagementContainer() override = default; |
| |
| views::SizeBounds GetAvailableSize(const View* child) const override { |
| // Only consider setting a bounded available size for |
| // `GlanceablesTimeManagementBubbleView` children. |
| auto* time_management_child = |
| views::AsViewClass<GlanceablesTimeManagementBubbleView>(child); |
| if (!time_management_child || !time_management_child->IsExpanded()) { |
| return views::SizeBounds(); |
| } |
| |
| const auto container_available_height = |
| parent()->GetAvailableSize(this).height(); |
| if (!container_available_height.is_bounded()) { |
| return views::SizeBounds(); |
| } |
| |
| int available_height = |
| container_available_height.value() - GetInteriorMargin().height(); |
| bool is_first_visible_child = true; |
| for (auto child_iter : children()) { |
| if (!child_iter->GetVisible()) { |
| continue; |
| } |
| auto* typed_child = |
| views::AsViewClass<GlanceablesTimeManagementBubbleView>(child_iter); |
| if (!typed_child) { |
| continue; |
| } |
| if (!is_first_visible_child) { |
| available_height -= 8; |
| } |
| // Assume that only one GlanceablesTimeManagementBubbleView is expanded. |
| if (child_iter != time_management_child) { |
| available_height -= typed_child->GetCollapsedStatePreferredHeight(); |
| } |
| is_first_visible_child = false; |
| } |
| |
| views::SizeBounds available_size; |
| available_size.set_height(available_height); |
| return available_size; |
| } |
| |
| void ChildPreferredSizeChanged(views::View* child) override { |
| PreferredSizeChanged(); |
| } |
| |
| void ChildVisibilityChanged(views::View* child) override { |
| PreferredSizeChanged(); |
| } |
| }; |
| |
| BEGIN_METADATA(TimeManagementContainer) |
| END_METADATA |
| |
| } // namespace |
| |
| // static |
| void GlanceableTrayBubbleView::RegisterUserProfilePrefs( |
| PrefRegistrySimple* registry) { |
| registry->RegisterIntegerPref( |
| prefs::kGlanceablesTimeManagementLastExpandedBubble, |
| base::to_underlying(GlanceablesContext::kTasks)); |
| } |
| |
| // static |
| void GlanceableTrayBubbleView::ClearUserStatePrefs(PrefService* prefs) { |
| prefs->ClearPref(prefs::kGlanceablesTimeManagementLastExpandedBubble); |
| } |
| |
| GlanceableTrayBubbleView::GlanceableTrayBubbleView( |
| const InitParams& init_params, |
| Shelf* shelf) |
| : TrayBubbleView(init_params), shelf_(shelf) { |
| Shell::Get()->glanceables_controller()->RecordGlanceablesBubbleShowTime( |
| base::TimeTicks::Now()); |
| // The calendar view should always keep its size if possible. If there is no |
| // enough space, the `time_management_container_view_` should be prioritized |
| // to be shrunk. Set the default flex to 0 and manually updates the flex of |
| // views depending on the view hierarchy. |
| box_layout()->SetDefaultFlex(0); |
| box_layout()->set_between_child_spacing(kMarginBetweenGlanceables); |
| } |
| |
| GlanceableTrayBubbleView::~GlanceableTrayBubbleView() { |
| Shell::Get()->glanceables_controller()->NotifyGlanceablesBubbleClosed(); |
| } |
| |
| void GlanceableTrayBubbleView::InitializeContents() { |
| CHECK(!initialized_); |
| |
| // TODO(b/286941809): Apply rounded corners. Temporary removed because they |
| // make the background blur to disappear and this requires further |
| // investigation. |
| |
| const auto* const session_controller = Shell::Get()->session_controller(); |
| CHECK(session_controller); |
| const bool should_show_non_calendar_glanceables = |
| session_controller->IsActiveUserSessionStarted() && |
| session_controller->GetSessionState() == |
| session_manager::SessionState::ACTIVE && |
| session_controller->GetUserSession(0)->user_info.has_gaia_account; |
| |
| if (!calendar_view_) { |
| calendar_container_ = |
| AddChildView(std::make_unique<views::FlexLayoutView>()); |
| calendar_view_ = |
| calendar_container_->AddChildView(std::make_unique<CalendarView>( |
| /*use_glanceables_container_style=*/true)); |
| SetCalendarPreferredSize(); |
| } |
| |
| auto* const tasks_client = |
| Shell::Get()->glanceables_controller()->GetTasksClient(); |
| if (should_show_non_calendar_glanceables && |
| features::IsGlanceablesTimeManagementTasksViewEnabled() && tasks_client && |
| !tasks_client->IsDisabledByAdmin()) { |
| CHECK(!tasks_bubble_view_); |
| auto* cached_list = tasks_client->GetCachedTaskLists(); |
| if (!cached_list) { |
| tasks_client->GetTaskLists( |
| /*force_fetch=*/true, |
| base::BindOnce(&GlanceableTrayBubbleView::AddTaskBubbleViewIfNeeded, |
| weak_ptr_factory_.GetWeakPtr())); |
| } else { |
| AddTaskBubbleViewIfNeeded(/*fetch_success=*/true, |
| google_apis::ApiErrorCode::HTTP_SUCCESS, |
| cached_list); |
| tasks_client->GetTaskLists( |
| /*force_fetch=*/true, |
| base::BindOnce(&GlanceableTrayBubbleView::UpdateTaskLists, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| } |
| |
| const int max_height = CalculateMaxTrayBubbleHeight(shelf_->GetWindow()); |
| SetMaxHeight(max_height); |
| ChangeAnchorAlignment(shelf_->alignment()); |
| ChangeAnchorRect(shelf_->GetSystemTrayAnchorRect()); |
| |
| auto* const classroom_client = |
| Shell::Get()->glanceables_controller()->GetClassroomClient(); |
| if (should_show_non_calendar_glanceables && |
| features::IsGlanceablesTimeManagementClassroomStudentViewEnabled() && |
| classroom_client && !classroom_client->IsDisabledByAdmin()) { |
| CHECK(!classroom_bubble_student_view_); |
| classroom_client->IsStudentRoleActive(base::BindOnce( |
| &GlanceableTrayBubbleView::AddClassroomBubbleStudentViewIfNeeded, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| // Layout to set the calendar view bounds, so the calendar view finishes |
| // initializing (e.g. scroll to today), which happens when the calendar view |
| // bounds are set. |
| DeprecatedLayoutImmediately(); |
| |
| initialized_ = true; |
| } |
| |
| gfx::Size GlanceableTrayBubbleView::CalculatePreferredSize( |
| const views::SizeBounds& available_size) const { |
| int width = TrayBubbleView::CalculatePreferredSize(available_size).width(); |
| // Let the layout manager calculate the preferred height instead of using the |
| // one from TrayBubbleView, which doesn't take the layout manager and margin |
| // settings into consider. |
| return gfx::Size( |
| width, |
| std::min(GetLayoutManager()->GetPreferredHeightForWidth(this, width), |
| CalculateMaxTrayBubbleHeight(shelf_->GetWindow()))); |
| } |
| |
| views::SizeBounds GlanceableTrayBubbleView::GetAvailableSize( |
| const View* child) const { |
| if (child != time_management_container_view_) { |
| return TrayBubbleView::GetAvailableSize(child); |
| } |
| |
| views::SizeBounds available_size; |
| auto max_height = CalculateMaxTrayBubbleHeight(shelf_->GetWindow()); |
| available_size.set_height(max_height - |
| calendar_view_->GetPreferredSize().height() - |
| kMarginBetweenGlanceables); |
| return available_size; |
| } |
| |
| void GlanceableTrayBubbleView::AddedToWidget() { |
| if (!initialized_) { |
| InitializeContents(); |
| } |
| TrayBubbleView::AddedToWidget(); |
| } |
| |
| void GlanceableTrayBubbleView::OnWidgetClosing(views::Widget* widget) { |
| if (tasks_bubble_view_) { |
| tasks_bubble_view_->CancelUpdates(); |
| } |
| if (classroom_bubble_student_view_) { |
| classroom_bubble_student_view_->CancelUpdates(); |
| } |
| |
| TrayBubbleView::OnWidgetClosing(widget); |
| } |
| |
| void GlanceableTrayBubbleView::OnDidApplyDisplayChanges() { |
| int max_height = CalculateMaxTrayBubbleHeight(shelf_->GetWindow()); |
| SetMaxHeight(max_height); |
| SetCalendarPreferredSize(); |
| ChangeAnchorRect(shelf_->GetSystemTrayAnchorRect()); |
| } |
| |
| void GlanceableTrayBubbleView::OnExpandStateChanged(GlanceablesContext context, |
| bool is_expanded, |
| bool expand_by_overscroll) { |
| // If one of the `GlanceablesTimeManagementBubbleView` is expanded, collapse |
| // the other. |
| if (context == GlanceablesContext::kClassroom && tasks_bubble_view_) { |
| tasks_bubble_view_->SetExpandState(!is_expanded, expand_by_overscroll); |
| SetLastExpandedGlanceables(is_expanded ? GlanceablesContext::kClassroom |
| : GlanceablesContext::kTasks); |
| return; |
| } |
| |
| if (context == GlanceablesContext::kTasks && classroom_bubble_student_view_) { |
| classroom_bubble_student_view_->SetExpandState(!is_expanded, |
| expand_by_overscroll); |
| SetLastExpandedGlanceables(is_expanded ? GlanceablesContext::kTasks |
| : GlanceablesContext::kClassroom); |
| return; |
| } |
| } |
| |
| void GlanceableTrayBubbleView::AddClassroomBubbleStudentViewIfNeeded( |
| bool is_role_active) { |
| if (!is_role_active) { |
| return; |
| } |
| |
| // Adds classroom bubble before `calendar_view_`. |
| MaybeCreateTimeManagementContainer(); |
| classroom_bubble_student_view_ = |
| time_management_container_view_->AddChildView( |
| std::make_unique<GlanceablesClassroomStudentView>()); |
| time_management_view_observation_.AddObservation( |
| classroom_bubble_student_view_); |
| // If `tasks_bubble_view_` exists, collapse either `tasks_bubble_view_` or |
| // `classroom_bubble_student_view_` according to the prefs. |
| if (tasks_bubble_view_) { |
| UpdateChildBubblesInitialExpandState(); |
| } |
| |
| UpdateTimeManagementContainerLayout(); |
| UpdateBubble(); |
| |
| AdjustChildrenFocusOrder(); |
| } |
| |
| void GlanceableTrayBubbleView::AddTaskBubbleViewIfNeeded( |
| bool fetch_success, |
| std::optional<google_apis::ApiErrorCode> http_error, |
| const ui::ListModel<api::TaskList>* task_lists) { |
| if (!fetch_success || task_lists->item_count() == 0) { |
| return; |
| } |
| |
| // Add tasks bubble before everything. |
| MaybeCreateTimeManagementContainer(); |
| tasks_bubble_view_ = time_management_container_view_->AddChildViewAt( |
| std::make_unique<GlanceablesTasksView>(task_lists), 0); |
| time_management_view_observation_.AddObservation(tasks_bubble_view_); |
| // If `classroom_bubble_student_view_` exists, collapse either |
| // `tasks_bubble_view_` or `classroom_bubble_student_view_` according to the |
| // prefs. |
| if (classroom_bubble_student_view_) { |
| UpdateChildBubblesInitialExpandState(); |
| } |
| |
| UpdateTimeManagementContainerLayout(); |
| UpdateBubble(); |
| |
| AdjustChildrenFocusOrder(); |
| } |
| |
| void GlanceableTrayBubbleView::UpdateChildBubblesInitialExpandState() { |
| // By default all children is in expanded states. Directly collapse the one |
| // that should be collapsed. Also we only have to update the expand state of |
| // one child bubble as `OnExpandStateChanged()` will automatically updates the |
| // others. |
| if (GetLastExpandedGlanceables() == GlanceablesContext::kTasks) { |
| classroom_bubble_student_view_->SetExpandState( |
| false, /*expand_by_overscroll=*/false); |
| } else { |
| tasks_bubble_view_->SetExpandState(false, /*expand_by_overscroll=*/false); |
| } |
| } |
| |
| void GlanceableTrayBubbleView::UpdateTaskLists( |
| bool fetch_success, |
| std::optional<google_apis::ApiErrorCode> http_error, |
| const ui::ListModel<api::TaskList>* task_lists) { |
| if (fetch_success && |
| features::IsGlanceablesTimeManagementTasksViewEnabled()) { |
| views::AsViewClass<GlanceablesTasksView>(tasks_bubble_view_) |
| ->UpdateTaskLists(task_lists); |
| } |
| } |
| |
| void GlanceableTrayBubbleView::AdjustChildrenFocusOrder() { |
| auto* default_focused_child = GetChildrenFocusList().front().get(); |
| |
| // Make sure the view that contains calendar is the first in the focus list of |
| // glanceable views. |
| if (default_focused_child != calendar_container_) { |
| calendar_container_->InsertBeforeInFocusList(default_focused_child); |
| } |
| } |
| |
| void GlanceableTrayBubbleView::SetCalendarPreferredSize() const { |
| // TODO(b/312320532): Update the height if display height is less than |
| // `kCalendarBubbleHeightSmallDisplay`. |
| calendar_view_->SetPreferredSize(gfx::Size( |
| kWideTrayMenuWidth, CalculateMaxTrayBubbleHeight(shelf_->GetWindow()) > |
| kDisplayHeightThreshold |
| ? kCalendarBubbleHeightLargeDisplay |
| : kCalendarBubbleHeightSmallDisplay)); |
| } |
| |
| void GlanceableTrayBubbleView::MaybeCreateTimeManagementContainer() { |
| if (!time_management_container_view_) { |
| time_management_container_view_ = |
| AddChildViewAt(std::make_unique<TimeManagementContainer>(), 0); |
| box_layout()->SetFlexForView(time_management_container_view_, 1); |
| } |
| } |
| |
| void GlanceableTrayBubbleView::UpdateTimeManagementContainerLayout() { |
| if (time_management_container_view_->children().size() > 1) { |
| tasks_bubble_view_->CreateElevatedBackground(); |
| classroom_bubble_student_view_->CreateElevatedBackground(); |
| } |
| } |
| |
| BEGIN_METADATA(GlanceableTrayBubbleView) |
| END_METADATA |
| |
| } // namespace ash |