chromium/ash/system/unified/unified_slider_bubble_controller.cc

// Copyright 2018 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/unified_slider_bubble_controller.h"

#include "ash/bubble/bubble_constants.h"
#include "ash/constants/ash_features.h"
#include "ash/rgb_keyboard/rgb_keyboard_manager.h"
#include "ash/root_window_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/system/audio/mic_gain_slider_controller.h"
#include "ash/system/brightness/unified_brightness_slider_controller.h"
#include "ash/system/keyboard_brightness/keyboard_backlight_color_controller.h"
#include "ash/system/keyboard_brightness/keyboard_backlight_color_nudge_controller.h"
#include "ash/system/keyboard_brightness/keyboard_backlight_toggle_controller.h"
#include "ash/system/keyboard_brightness/unified_keyboard_brightness_slider_controller.h"
#include "ash/system/status_area_widget.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/tray/tray_utils.h"
#include "ash/system/unified/unified_slider_view.h"
#include "ash/system/unified/unified_system_tray.h"
#include "ash/system/unified/unified_system_tray_bubble.h"
#include "ash/system/video_conference/video_conference_tray.h"
#include "base/functional/bind.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/views/border.h"

namespace ash {

namespace {

using SliderType = UnifiedSliderBubbleController::SliderType;

// The padding of slider toast.
constexpr auto kQsSliderToastPadding = gfx::Insets::TLBR(8, 8, 8, 12);
constexpr auto kQsToggleToastPadding = gfx::Insets(12);
// The rounded corner radius of the `bubble_view_`.
constexpr int kQsToastCornerRadius = 28;

// Return true if a system tray bubble is shown in any display.
bool IsAnyMainBubbleShown() {
  for (RootWindowController* root : Shell::GetAllRootWindowControllers()) {
    if (root->GetStatusAreaWidget()->unified_system_tray()->IsBubbleShown()) {
      return true;
    }
  }
  return false;
}

void ConfigureSliderViewStyle(UnifiedSliderView* slider_view,
                              SliderType slider_type) {
  // Toggle toast has only a button and label. Slider toast has a slider, a
  // button on the slider body, and possible trailing buttons.
  const bool is_toggle_toast =
      slider_type == SliderType::SLIDER_TYPE_KEYBOARD_BACKLIGHT_TOGGLE_ON ||
      slider_type == SliderType::SLIDER_TYPE_KEYBOARD_BACKLIGHT_TOGGLE_OFF;
  auto* layout =
      slider_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
          views::BoxLayout::Orientation::kHorizontal,
          is_toggle_toast ? kQsToggleToastPadding : kQsSliderToastPadding,
          kSliderChildrenViewSpacing));
  layout->SetFlexForView(slider_view->slider(), /*flex=*/1);
  layout->set_cross_axis_alignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);
}

// Returns whether the `VideoConferenceTray` should be shown.
bool ShouldVideoConferenceTrayBeShown() {
  if (!features::IsVideoConferenceEnabled()) {
    return false;
  }

  // The tray is shown on every display, so just check the primary display.
  auto* status_area_widget =
      Shell::Get()->GetPrimaryRootWindowController()->GetStatusAreaWidget();
  // `UnifiedSliderBubbleController` belongs to `UnifiedSystemTray` which is
  // created during construction of `status_area_widget`, so it is possible
  // this is called before `status_area_widget` is created.
  return status_area_widget &&
         status_area_widget->video_conference_tray()->visible_preferred();
}

}  // namespace

UnifiedSliderBubbleController::UnifiedSliderBubbleController(
    UnifiedSystemTray* tray)
    : tray_(tray) {
  CrasAudioHandler::Get()->AddAudioObserver(this);
  tray_->model()->AddObserver(this);
  tray_->shelf()->AddObserver(this);
}

UnifiedSliderBubbleController::~UnifiedSliderBubbleController() {
  CrasAudioHandler::Get()->RemoveAudioObserver(this);
  tray_->model()->RemoveObserver(this);
  tray_->shelf()->RemoveObserver(this);
  autoclose_.Stop();
  slider_controller_.reset();
  if (bubble_widget_) {
    // Reset `slider_view_`
    // to prevent dangling pointer caused by view removal.
    // TODO(b/40280409): We shouldn't need this if child view removal is made
    // more safe.
    slider_view_ = nullptr;

    bubble_widget_->CloseNow();
  }
}

void UnifiedSliderBubbleController::CloseBubble() {
  autoclose_.Stop();
  slider_controller_.reset();
  if (!bubble_widget_) {
    return;
  }
  // Ignore the request if the bubble is closing.
  if (bubble_widget_->IsClosed()) {
    return;
  }
  bubble_widget_->Close();
  tray_->NotifySecondaryBubbleHeight(0);
}

bool UnifiedSliderBubbleController::IsBubbleShown() const {
  return !!bubble_widget_ && !bubble_widget_->IsClosed();
}

int UnifiedSliderBubbleController::GetBubbleHeight() const {
  return !!slider_view_ ? slider_view_->height() : 0;
}

void UnifiedSliderBubbleController::BubbleViewDestroyed() {
  slider_controller_.reset();
  slider_view_ = nullptr;
  bubble_view_ = nullptr;
  bubble_widget_ = nullptr;
}

void UnifiedSliderBubbleController::OnMouseEnteredView() {
  // If mouse if hovered, pause auto close timer until mouse moves out.
  autoclose_.Stop();
  mouse_hovered_ = true;
}

void UnifiedSliderBubbleController::OnMouseExitedView() {
  StartAutoCloseTimer();
  mouse_hovered_ = false;
}

void UnifiedSliderBubbleController::HideBubble(
    const TrayBubbleView* bubble_view) {}

void UnifiedSliderBubbleController::DisplayMicrophoneMuteToast() {
  // We will not display the microphone mute toast if no microphone is connected
  // to the device, or if the video conference controls tray is visible.
  if (CrasAudioHandler::Get()->HasActiveInputDeviceForSimpleUsage() &&
      !ShouldVideoConferenceTrayBeShown()) {
    ShowBubble(SLIDER_TYPE_MIC);
  }
}

void UnifiedSliderBubbleController::OnInputMuteChanged(
    bool mute,
    CrasAudioHandler::InputMuteChangeMethod method) {
  // The toast is displayed when the input mute state is changed by the
  // dedicated keyboard button.
  if (method == CrasAudioHandler::InputMuteChangeMethod::kKeyboardButton) {
    DisplayMicrophoneMuteToast();
  }
}

void UnifiedSliderBubbleController::OnInputMutedByMicrophoneMuteSwitchChanged(
    bool muted) {
  // The toast is displayed whenever the state of the hadrdware switch changes.
  DisplayMicrophoneMuteToast();
}

void UnifiedSliderBubbleController::OnOutputNodeVolumeChanged(uint64_t node_id,
                                                              int volume) {
  ShowBubble(SLIDER_TYPE_VOLUME);
}

void UnifiedSliderBubbleController::OnOutputMuteChanged(bool mute_on) {
  ShowBubble(SLIDER_TYPE_VOLUME);
}

void UnifiedSliderBubbleController::OnDisplayBrightnessChanged(bool by_user) {
  if (by_user) {
    ShowBubble(SLIDER_TYPE_DISPLAY_BRIGHTNESS);
  }
}

void UnifiedSliderBubbleController::OnKeyboardBrightnessChanged(
    power_manager::BacklightBrightnessChange_Cause cause) {
  if (cause == power_manager::BacklightBrightnessChange_Cause_USER_REQUEST) {
    // User has made a brightness adjustment, or the KBL was made
    // no-longer-forced-off implicitly in response to a user adjustment.
    ShowBubble(SLIDER_TYPE_KEYBOARD_BRIGHTNESS);
    if (Shell::Get()->rgb_keyboard_manager()->IsRgbKeyboardSupported()) {
      // Show the education nudge to change the keyboard backlight color if
      // applicable. |bubble_view_| is used as the anchor view.
      Shell::Get()
          ->keyboard_backlight_color_controller()
          ->keyboard_backlight_color_nudge_controller()
          ->MaybeShowEducationNudge(bubble_view_);
    }
  } else if (cause == power_manager::
                          BacklightBrightnessChange_Cause_USER_TOGGLED_OFF ||
             cause == power_manager::
                          BacklightBrightnessChange_Cause_USER_TOGGLED_ON) {
    // User has explicitly toggled the KBL backlight.
    ShowBubble((cause ==
                power_manager::BacklightBrightnessChange_Cause_USER_TOGGLED_OFF)
                   ? SLIDER_TYPE_KEYBOARD_BACKLIGHT_TOGGLE_OFF
                   : SLIDER_TYPE_KEYBOARD_BACKLIGHT_TOGGLE_ON);
  }
}

void UnifiedSliderBubbleController::OnAudioSettingsButtonClicked() {
  tray_->ShowAudioDetailedViewBubble();
}

void UnifiedSliderBubbleController::OnShelfWorkAreaInsetsChanged() {
  if (bubble_view_) {
    bubble_view_->ChangeAnchorRect(tray_->shelf()->GetSystemTrayAnchorRect());
  }
}

void UnifiedSliderBubbleController::ShowBubble(SliderType slider_type) {
  // Never show slider bubble in kiosk app mode.
  if (Shell::Get()->session_controller()->IsRunningInAppMode()) {
    return;
  }

  bool is_audio_slider = slider_type == SLIDER_TYPE_MIC;
  // both the volume slider and mic gain slider will be shown in
  // `AudioDetailedView`.
  is_audio_slider = is_audio_slider || slider_type == SLIDER_TYPE_VOLUME;

  // When tray bubble is already shown, the microphone slider will get shown in
  // audio detailed view. Bail out if the audio details are already showing to
  // avoid resetting the bubble state.
  // If already in the `AudioDetailedView`, bail out if it's either
  // `SLIDER_TYPE_MIC` or `SLIDER_TYPE_VOLUME`.
  if (is_audio_slider && tray_->bubble() &&
      tray_->bubble()->ShowingAudioDetailedView()) {
    return;
  }

  // When tray bubble is already shown, the brightness slider will get shown in
  // display detailed view. Bail out if the display details are already showing
  // to avoid resetting the bubble state.
  if (slider_type == SLIDER_TYPE_DISPLAY_BRIGHTNESS && tray_->bubble() &&
      tray_->bubble()->ShowingDisplayDetailedView()) {
    return;
  }

  if (IsAnyMainBubbleShown()) {
    // If a detailed view is showing, first transit to the main view.
    if (tray_->bubble() && tray_->bubble()->GetBubbleWidget()) {
      tray_->bubble()->unified_system_tray_controller()->TransitionToMainView(
          false);
    }

    // Unlike VOLUME and BRIGHTNESS, which are shown in the main bubble view,
    // MIC slider is shown in the audio details view.
    if (slider_type == SLIDER_TYPE_MIC && tray_->bubble()) {
      tray_->ShowAudioDetailedViewBubble();
    }
    return;
  }

  // Ignore the request if the bubble is closing.
  if (bubble_widget_ && bubble_widget_->IsClosed()) {
    return;
  }

  // If the bubble already exists, update the content of the bubble and extend
  // the autoclose timer.
  if (bubble_widget_) {
    CHECK(bubble_view_);

    if (slider_type_ != slider_type) {
      slider_type_ = slider_type;

      // Recreate the slider controller first to prevent dangling pointers when
      // removing child views.
      CreateSliderController();

      // `RemoveAllChildViews` will cause `slider_view_` to be dangling, so we
      // need to safely extract it.
      // TODO(b/40280409): We shouldn't need this if child view removal is made
      // more safe.
      slider_view_ = nullptr;
      bubble_view_->RemoveAllChildViews();

      slider_view_ = static_cast<UnifiedSliderView*>(
          bubble_view_->AddChildView(slider_controller_->CreateView()));
      ConfigureSliderViewStyle(slider_view_, slider_type);
      bubble_view_->DeprecatedLayoutImmediately();
    }

    // If mouse is hovered, do not restart auto close timer.
    if (!mouse_hovered_) {
      StartAutoCloseTimer();
    }
    return;
  }

  tray_->CloseSecondaryBubbles();

  CHECK(!bubble_view_);

  slider_type_ = slider_type;
  CreateSliderController();

  TrayBubbleView::InitParams init_params =
      CreateInitParamsForTrayBubble(tray_, /*anchor_to_shelf_corner=*/true);
  init_params.type = TrayBubbleView::TrayBubbleType::kSecondaryBubble;
  init_params.reroute_event_handler = false;

  // Use this controller as the delegate rather than the tray.
  init_params.delegate = GetWeakPtr();

  init_params.corner_radius = kQsToastCornerRadius;
  // `bubble_view_` is fully rounded, so sets it to be true and paints the
  // shadow on texture layer.
  init_params.has_large_corner_radius = true;

  bubble_view_ = new TrayBubbleView(init_params);
  bubble_view_->SetCanActivate(false);
  slider_view_ = static_cast<UnifiedSliderView*>(
      bubble_view_->AddChildView(slider_controller_->CreateView()));
  ConfigureSliderViewStyle(slider_view_, slider_type);

  bubble_widget_ = views::BubbleDialogDelegateView::CreateBubble(bubble_view_);

  TrayBackgroundView::InitializeBubbleAnimations(bubble_widget_);
  bubble_view_->InitializeAndShowBubble();

  // Notify value change accessibility event because the popup is triggered by
  // changing value using an accessor key like VolUp.
  slider_view_->slider()->NotifyAccessibilityEvent(
      ax::mojom::Event::kValueChanged, true);

  StartAutoCloseTimer();

  tray_->NotifySecondaryBubbleHeight(slider_view_->height());
}

void UnifiedSliderBubbleController::CreateSliderController() {
  switch (slider_type_) {
    case SLIDER_TYPE_VOLUME:
      slider_controller_ =
          std::make_unique<UnifiedVolumeSliderController>(this);
      return;
    case SLIDER_TYPE_DISPLAY_BRIGHTNESS:
      slider_controller_ = std::make_unique<UnifiedBrightnessSliderController>(
          tray_->model().get(),
          base::BindRepeating(&UnifiedSystemTray::ShowDisplayDetailedViewBubble,
                              base::Unretained(tray_)));
      return;
    case SLIDER_TYPE_KEYBOARD_BACKLIGHT_TOGGLE_OFF:
      slider_controller_ = std::make_unique<KeyboardBacklightToggleController>(
          tray_->model().get(), /*toggled_on=*/false);
      return;
    case SLIDER_TYPE_KEYBOARD_BACKLIGHT_TOGGLE_ON:
      slider_controller_ = std::make_unique<KeyboardBacklightToggleController>(
          tray_->model().get(), /*toggled_on=*/true);
      return;
    case SLIDER_TYPE_KEYBOARD_BRIGHTNESS:
      slider_controller_ =
          std::make_unique<UnifiedKeyboardBrightnessSliderController>(
              tray_->model().get());
      return;
    case SLIDER_TYPE_MIC:
      slider_controller_ = std::make_unique<MicGainSliderController>();
      return;
  }
}

void UnifiedSliderBubbleController::StartAutoCloseTimer() {
  autoclose_.Stop();
  autoclose_.Start(FROM_HERE, kSecondaryBubbleDuration, this,
                   &UnifiedSliderBubbleController::CloseBubble);
}

}  // namespace ash