chromium/ash/system/video_conference/bubble/bubble_view.cc

// Copyright 2022 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/video_conference/bubble/bubble_view.h"

#include <memory>
#include <string>

#include "ash/constants/ash_features.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/typography.h"
#include "ash/system/camera/camera_effects_controller.h"
#include "ash/system/tray/tray_bubble_view.h"
#include "ash/system/video_conference/bubble/bubble_view_ids.h"
#include "ash/system/video_conference/bubble/return_to_app_panel.h"
#include "ash/system/video_conference/bubble/set_camera_background_view.h"
#include "ash/system/video_conference/bubble/set_value_effects_view.h"
#include "ash/system/video_conference/bubble/title_view.h"
#include "ash/system/video_conference/bubble/toggle_effects_view.h"
#include "ash/system/video_conference/effects/video_conference_tray_effects_manager.h"
#include "ash/system/video_conference/video_conference_tray_controller.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "chromeos/crosapi/mojom/video_conference.mojom.h"
#include "media/capture/video/chromeos/mojom/effects_pipeline.mojom.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/views/border.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_view.h"
#include "ui/views/view_utils.h"

namespace ash::video_conference {

namespace {

constexpr int kLinuxAppWarningViewTopPadding = 12;
constexpr int kDLCErrorWarningLabelTopPadding = 0;
constexpr int kWarningViewSpacing = 1;
constexpr int kWarningIconSize = 16;

constexpr int kScrollViewBetweenChildSpacing = 16;

CameraEffectsController* GetCameraEffectsController() {
  return Shell::Get()->camera_effects_controller();
}

// Check if there's a linux app in the given `apps`.
bool HasLinuxApps(const MediaApps& apps) {
  for (auto& app : apps) {
    if (app->app_type == crosapi::mojom::VideoConferenceAppType::kCrostiniVm ||
        app->app_type == crosapi::mojom::VideoConferenceAppType::kPluginVm ||
        app->app_type == crosapi::mojom::VideoConferenceAppType::kBorealis) {
      return true;
    }
  }
  return false;
}

// Creates a view that will display a warning icon with text.
std::unique_ptr<views::View> CreateWarningView(
    int warning_view_id,
    int top_padding,
    std::optional<int> warning_message = std::nullopt) {
  auto view = std::make_unique<views::View>();
  view->SetID(warning_view_id);
  view->SetLayoutManager(std::make_unique<views::FlexLayout>())
      ->SetOrientation(views::LayoutOrientation::kHorizontal)
      .SetMainAxisAlignment(views::LayoutAlignment::kCenter)
      .SetCrossAxisAlignment(views::LayoutAlignment::kStretch)
      .SetInteriorMargin(gfx::Insets::TLBR(top_padding, 0, 0, 0))
      .SetDefault(
          views::kMarginsKey,
          gfx::Insets::TLBR(0, kWarningViewSpacing, 0, kWarningViewSpacing));

  auto icon = std::make_unique<views::ImageView>();
  icon->SetImage(ui::ImageModel::FromVectorIcon(
      kVideoConferenceWarningIcon, cros_tokens::kCrosSysOnSurfaceVariant,
      kWarningIconSize));
  view->AddChildView(std::move(icon));

  auto label = std::make_unique<views::Label>();
  // Set a view ID so the label can be modified if necessary.
  label->SetID(BubbleViewID::kWarningViewLabel);
  if (warning_message.has_value()) {
    label->SetText(l10n_util::GetStringUTF16(*warning_message));
  }
  TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosAnnotation2,
                                        *label);
  view->AddChildView(std::move(label));

  return view;
}

}  // namespace

BubbleView::BubbleView(const InitParams& init_params,
                       const MediaApps& media_apps,
                       VideoConferenceTrayController* controller)
    : TrayBubbleView(init_params),
      controller_(controller),
      media_apps_(media_apps) {
  SetID(BubbleViewID::kMainBubbleView);

  // Add a `FlexLayout` for the entire view.
  SetLayoutManager(std::make_unique<views::FlexLayout>())
      ->SetOrientation(views::LayoutOrientation::kVertical)
      .SetMainAxisAlignment(views::LayoutAlignment::kCenter)
      .SetCrossAxisAlignment(views::LayoutAlignment::kStretch);
}

BubbleView::~BubbleView() = default;

void BubbleView::AddedToWidget() {
  if (features::IsVcTrayTitleHeaderEnabled()) {
    AddChildView(std::make_unique<TitleView>());
  }
  // `ReturnToAppPanel` resides in the top-level layout and isn't part of the
  // scrollable area (that can't be added until the `BubbleView` officially has
  // a parent widget).
  AddChildView(std::make_unique<ReturnToAppPanel>(*media_apps_));

  const bool has_toggle_effects =
      controller_->GetEffectsManager().HasToggleEffects();
  const bool has_set_value_effects =
      controller_->GetEffectsManager().HasSetValueEffects();

  if (HasLinuxApps(*media_apps_) &&
      (has_toggle_effects || has_set_value_effects)) {
    AddChildView(CreateWarningView(
        BubbleViewID::kLinuxAppWarningView,
        /*top_padding=*/kLinuxAppWarningViewTopPadding,
        IDS_ASH_VIDEO_CONFERENCE_BUBBLE_LINUX_APP_WARNING_TEXT));
  }

  // Create the `views::ScrollView` to house the effects sections. This has to
  // be done here because `BubbleDialogDelegate::GetBubbleBounds` requires a
  // parent widget, which isn't officially assigned until after the call to
  // `ShowBubble` in `VideoConferenceTray::ToggleBubble`.
  auto* scroll_view = AddChildView(std::make_unique<views::ScrollView>());
  scroll_view->SetAllowKeyboardScrolling(false);
  scroll_view->SetBackgroundColor(std::nullopt);

  // TODO(b/262930924): Use the correct max_height.
  scroll_view->ClipHeightTo(/*min_height=*/0, /*max_height=*/400);
  scroll_view->SetDrawOverflowIndicator(false);
  scroll_view->SetVerticalScrollBarMode(
      views::ScrollView::ScrollBarMode::kHiddenButEnabled);

  auto* scroll_contents_view =
      scroll_view->SetContents(std::make_unique<views::BoxLayoutView>());
  scroll_contents_view->SetOrientation(
      views::BoxLayout::Orientation::kVertical);
  scroll_contents_view->SetCrossAxisAlignment(
      views::BoxLayout::CrossAxisAlignment::kStretch);
  scroll_contents_view->SetInsideBorderInsets(
      gfx::Insets::VH(16, kVideoConferenceBubbleHorizontalPadding));
  scroll_contents_view->SetBetweenChildSpacing(kScrollViewBetweenChildSpacing);

  // Make the effects sections children of the `views::FlexLayoutView`, so that
  // they scroll (if more effects are present than can fit in the available
  // height).
  if (has_toggle_effects) {
    scroll_contents_view->AddChildView(
        std::make_unique<ToggleEffectsView>(controller_));
    auto error_warning_label_container_view =
        CreateWarningView(BubbleViewID::kDLCDownloadsInErrorView,
                          /*top_padding=*/kDLCErrorWarningLabelTopPadding);
    // Visibility will for most cases be false, if a DLC has an error in
    // downloading, state updates will be fetched by any toggle effect's
    // `FeatureTile`, then pushed from the controller via
    // `OnDLCDownloadStateInError()`.
    error_warning_label_container_view->SetVisible(false);
    scroll_contents_view->AddChildView(
        std::move(error_warning_label_container_view));
  }
  if (has_set_value_effects) {
    scroll_contents_view->AddChildView(
        std::make_unique<SetValueEffectsView>(controller_));
  }

  if (GetCameraEffectsController()->is_eligible_for_background_replace()) {
    set_camera_background_view_ = scroll_contents_view->AddChildView(
        std::make_unique<SetCameraBackgroundView>(this, controller_.get()));
  }
}

void BubbleView::ChildPreferredSizeChanged(View* child) {
  PreferredSizeChanged();
  SizeToContents();
}

bool BubbleView::CanActivate() const {
  return true;
}

void BubbleView::SetBackgroundReplaceUiVisible(bool visible) {
  CHECK(GetCameraEffectsController()->is_eligible_for_background_replace() &&
        set_camera_background_view_)
      << "Can't show set_camera_background_view before it is constructed.";

  views::AsViewClass<SetCameraBackgroundView>(set_camera_background_view_)
      ->SetBackgroundReplaceUiVisible(visible);
  ChildPreferredSizeChanged(set_camera_background_view_);
}

void BubbleView::OnDLCDownloadStateInError(
    bool add_warning_view,
    const std::u16string& feature_tile_title_string) {
  auto* dlc_error_container_view =
      GetViewByID(BubbleViewID::kDLCDownloadsInErrorView);
  if (!dlc_error_container_view) {
    return;
  }

  auto* dlc_error_label = static_cast<views::Label*>(
      dlc_error_container_view->GetViewByID(BubbleViewID::kWarningViewLabel));
  if (!dlc_error_label) {
    return;
  }

  if (add_warning_view) {
    if (std::size(feature_tile_error_string_ids_) == 2) {
      return;
    }
    feature_tile_error_string_ids_.emplace(feature_tile_title_string);
  } else {
    auto it = std::find(feature_tile_error_string_ids_.begin(),
                        feature_tile_error_string_ids_.end(),
                        feature_tile_title_string);
    if (it == feature_tile_error_string_ids_.end()) {
      return;
    }
    feature_tile_error_string_ids_.erase(it);
  }

  // If the one and only string was removed, hide the view and reset it.
  // Otherwise update the string.
  if (feature_tile_error_string_ids_.empty()) {
    dlc_error_container_view->SetVisible(false);
    dlc_error_label->SetText(std::u16string());
    return;
  }

  if (feature_tile_error_string_ids_.size() == 1) {
    dlc_error_label->SetText(l10n_util::GetStringFUTF16(
        IDS_ASH_VIDEO_CONFERENCE_BUBBLE_DLC_ERROR_ONE,
        *feature_tile_error_string_ids_.begin()));
    dlc_error_container_view->SetVisible(true);
    return;
  }

  // Only two are supported, adding more would require more custom handling for
  // the string below.
  if (feature_tile_error_string_ids_.size() > 2u) {
    return;
  }
  std::vector<std::u16string> string_ids(feature_tile_error_string_ids_.size());
  std::copy(feature_tile_error_string_ids_.begin(),
            feature_tile_error_string_ids_.end(), string_ids.begin());
  dlc_error_label->SetText(
      l10n_util::GetStringFUTF16(IDS_ASH_VIDEO_CONFERENCE_BUBBLE_DLC_ERROR_TWO,
                                 string_ids, /*offsets=*/nullptr));
  dlc_error_container_view->SetVisible(true);
}

BEGIN_METADATA(BubbleView)
END_METADATA

}  // namespace ash::video_conference