| // 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. |
| |
| #include "ash/capture_mode/capture_mode_settings_view.h" |
| |
| #include <memory> |
| #include <string> |
| |
| #include "ash/capture_mode/capture_mode_bar_view.h" |
| #include "ash/capture_mode/capture_mode_constants.h" |
| #include "ash/capture_mode/capture_mode_controller.h" |
| #include "ash/capture_mode/capture_mode_menu_toggle_button.h" |
| #include "ash/capture_mode/capture_mode_metrics.h" |
| #include "ash/capture_mode/capture_mode_session.h" |
| #include "ash/capture_mode/capture_mode_session_focus_cycler.h" |
| #include "ash/capture_mode/capture_mode_types.h" |
| #include "ash/capture_mode/capture_mode_util.h" |
| #include "ash/constants/ash_features.h" |
| #include "ash/public/cpp/style/color_provider.h" |
| #include "ash/resources/vector_icons/vector_icons.h" |
| #include "ash/screen_util.h" |
| #include "ash/strings/grit/ash_strings.h" |
| #include "ash/style/ash_color_id.h" |
| #include "ash/style/icon_button.h" |
| #include "ash/style/system_shadow.h" |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "chromeos/constants/chromeos_features.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/views/background.h" |
| #include "ui/views/controls/separator.h" |
| #include "ui/views/highlight_border.h" |
| #include "ui/views/layout/box_layout.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| constexpr int kCornerRadius = 10; |
| constexpr gfx::RoundedCornersF kRoundedCorners{kCornerRadius}; |
| |
| constexpr gfx::Size kSettingsSize{266, 248}; |
| |
| // Returns the bounds of the settings widget in screen coordinates relative to |
| // the bounds of the `bar_view` based on its given preferred |
| // `settings_size`, which should be centered with respect to the capture |
| // bar. |
| // |
| // The bounds priority works as follows: |
| // - If there is enough space above the bar view, we will show the menu at its |
| // full height. |
| // - Otherwise, we will choose between showing above or below the bar, |
| // whichever has more space. The available space only includes the work area, |
| // as we do not want to show the menu on top of or behind the shelf. |
| // - If necessary, we will also constrain the height of the menu, up to |
| // `capture_mode::kSettingsMenuMinHeight`. |
| gfx::Rect GetWidgetBounds(CaptureModeBarView* bar_view, |
| const gfx::Size& settings_size) { |
| const int width = settings_size.width(); |
| const int pref_height = settings_size.height(); |
| |
| const gfx::Rect bar_bounds = bar_view->GetBoundsInScreen(); |
| const int x = bar_bounds.CenterPoint().x() - width / 2.f; |
| int menu_bottom = |
| bar_bounds.y() - capture_mode::kSpaceBetweenCaptureBarAndSettingsMenu; |
| int y = menu_bottom - pref_height; |
| |
| // Showing the menu above the bar at full height is our priority, but this may |
| // change if it is too close to the top of the screen. |
| if (y < capture_mode::kMinDistanceFromSettingsToScreen) { |
| const gfx::Rect work_area = |
| screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer( |
| bar_view->GetWidget()->GetNativeWindow()); |
| const int available_above = menu_bottom - |
| capture_mode::kMinDistanceFromSettingsToScreen - |
| work_area.y(); |
| const int available_below = |
| work_area.y() + work_area.height() - |
| capture_mode::kMinDistanceFromSettingsToScreen - |
| capture_mode::kSpaceBetweenCaptureBarAndSettingsMenu - bar_bounds.y() - |
| bar_bounds.height(); |
| |
| // We want to show the menu on the side of the bar that has more space. |
| if (available_above >= available_below) { |
| y = std::max( |
| bar_bounds.y() - |
| capture_mode::kSpaceBetweenCaptureBarAndSettingsMenu - |
| pref_height, |
| work_area.y() + capture_mode::kMinDistanceFromSettingsToScreen); |
| menu_bottom = |
| bar_bounds.y() - capture_mode::kSpaceBetweenCaptureBarAndSettingsMenu; |
| } else { |
| y = bar_bounds.bottom() + |
| capture_mode::kSpaceBetweenCaptureBarAndSettingsMenu; |
| menu_bottom = std::min( |
| y + pref_height, |
| work_area.bottom() - capture_mode::kMinDistanceFromSettingsToScreen); |
| } |
| } |
| |
| return gfx::Rect( |
| x, y, width, |
| std::max(capture_mode::kSettingsMenuMinHeight, menu_bottom - y)); |
| } |
| |
| CaptureModeController::CaptureFolder GetCurrentCaptureFolder() { |
| return CaptureModeController::Get()->GetCurrentCaptureFolder(); |
| } |
| |
| } // namespace |
| |
| CaptureModeSettingsView::CaptureModeSettingsView( |
| CaptureModeSession* session, |
| CaptureModeBehavior* active_behavior) |
| : ScrollView(views::ScrollView::ScrollWithLayers::kEnabled), |
| capture_mode_session_(session), |
| active_behavior_(active_behavior), |
| shadow_(SystemShadow::CreateShadowOnNinePatchLayerForView( |
| this, |
| SystemShadow::Type::kElevation12)) { |
| auto* controller = CaptureModeController::Get(); |
| |
| SetContents(std::make_unique<views::View>()); |
| |
| if (controller->can_start_new_recording()) { |
| const bool audio_capture_managed_by_policy = |
| controller->IsAudioCaptureDisabledByPolicy(); |
| |
| DCHECK( |
| !audio_capture_managed_by_policy || |
| active_behavior->SupportsAudioRecordingMode(AudioRecordingMode::kOff)) |
| << "A client session should not be allowed to begin if audio " |
| "recording is diabled by policy."; |
| |
| audio_input_menu_group_ = |
| contents()->AddChildView(std::make_unique<CaptureModeMenuGroup>( |
| this, kCaptureModeMicIcon, |
| l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_AUDIO_INPUT), |
| audio_capture_managed_by_policy)); |
| |
| // A list of all the possible audio options. |
| struct { |
| // The backend audio recording mode for this option. |
| AudioRecordingMode audio_recording_mode; |
| // The ID of this menu group option. |
| int option_id; |
| // The ID of the string that will be used for the option's label. |
| int string_id; |
| // True if the option can be added if audio recording is managed by an |
| // admin policy. |
| bool add_if_managed_by_policy; |
| } kAudioOptions[] = { |
| {AudioRecordingMode::kOff, kAudioOff, |
| IDS_ASH_SCREEN_CAPTURE_AUDIO_INPUT_OFF, |
| /*add_if_managed_by_policy=*/true}, |
| {AudioRecordingMode::kSystem, kAudioSystem, |
| IDS_ASH_SCREEN_CAPTURE_AUDIO_INPUT_SYSTEM, |
| /*add_if_managed_by_policy=*/false}, |
| {AudioRecordingMode::kMicrophone, kAudioMicrophone, |
| IDS_ASH_SCREEN_CAPTURE_AUDIO_INPUT_MICROPHONE, |
| /*add_if_managed_by_policy=*/false}, |
| {AudioRecordingMode::kSystemAndMicrophone, kAudioSystemAndMicrophone, |
| IDS_ASH_SCREEN_CAPTURE_AUDIO_INPUT_SYSTEM_AND_MICROPHONE, |
| /*add_if_managed_by_policy=*/false}, |
| }; |
| |
| for (const auto& audio_option : kAudioOptions) { |
| if ((!audio_capture_managed_by_policy || |
| audio_option.add_if_managed_by_policy) && |
| active_behavior->SupportsAudioRecordingMode( |
| audio_option.audio_recording_mode)) { |
| audio_input_menu_group_->AddOption( |
| /*option_icon=*/nullptr, |
| l10n_util::GetStringUTF16(audio_option.string_id), |
| audio_option.option_id); |
| } |
| } |
| |
| separator_1_ = |
| contents()->AddChildView(std::make_unique<views::Separator>()); |
| separator_1_->SetColorId(ui::kColorAshSystemUIMenuSeparator); |
| auto* camera_controller = controller->camera_controller(); |
| const bool camera_managed_by_policy = |
| camera_controller->IsCameraDisabledByPolicy(); |
| // Even if the camera feature is managed by policy, we still want to observe |
| // the camera controller, since we need to be notified with camera additions |
| // and removals, which affect the visibility of the `camera_menu_group_`. |
| camera_controller->AddObserver(this); |
| camera_menu_group_ = |
| contents()->AddChildView(std::make_unique<CaptureModeMenuGroup>( |
| this, kCaptureModeCameraIcon, |
| l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_CAMERA), |
| camera_managed_by_policy)); |
| |
| AddCameraOptions(camera_controller->available_cameras(), |
| camera_managed_by_policy); |
| } |
| |
| if (controller->can_start_new_recording()) { |
| separator_2_ = |
| contents()->AddChildView(std::make_unique<views::Separator>()); |
| separator_2_->SetColorId(ui::kColorAshSystemUIMenuSeparator); |
| demo_tools_menu_toggle_button_ = |
| contents()->AddChildView(std::make_unique<CaptureModeMenuToggleButton>( |
| kCaptureModeDemoToolsSettingsMenuEntryPointIcon, |
| l10n_util::GetStringUTF16( |
| IDS_ASH_SCREEN_CAPTURE_DEMO_TOOLS_SHOW_CLICKS_AND_KEYS), |
| CaptureModeController::Get()->enable_demo_tools(), |
| base::BindRepeating( |
| &CaptureModeSettingsView::OnDemoToolsButtonToggled, |
| base::Unretained(this)))); |
| } |
| |
| if (active_behavior->ShouldSaveToSettingsBeIncluded()) { |
| separator_3_ = |
| contents()->AddChildView(std::make_unique<views::Separator>()); |
| separator_3_->SetColorId(ui::kColorAshSystemUIMenuSeparator); |
| |
| const bool custom_folder_managed_by_policy = |
| controller->IsCustomFolderManagedByPolicy(); |
| save_to_menu_group_ = |
| contents()->AddChildView(std::make_unique<CaptureModeMenuGroup>( |
| this, kCaptureModeFolderIcon, |
| l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_SAVE_TO), |
| /*enabled=*/custom_folder_managed_by_policy)); |
| save_to_menu_group_->AddOption( |
| /*option_icon=*/nullptr, |
| l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_SAVE_TO_DOWNLOADS), |
| kDownloadsFolder); |
| save_to_menu_group_->AddMenuItem( |
| base::BindRepeating( |
| &CaptureModeSettingsView::OnSelectFolderMenuItemPressed, |
| base::Unretained(this)), |
| l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_SAVE_TO_SELECT_FOLDER), |
| /*enabled=*/!custom_folder_managed_by_policy); |
| } |
| |
| SetBackground(views::CreateThemedSolidBackground(kColorAshShieldAndBase80)); |
| layer()->SetFillsBoundsOpaquely(false); |
| layer()->SetRoundedCornerRadius(kRoundedCorners); |
| layer()->SetBackgroundBlur(ColorProvider::kBackgroundBlurSigma); |
| layer()->SetBackdropFilterQuality(ColorProvider::kBackgroundBlurQuality); |
| |
| // The options should appear vertically stacked on top of each other. |
| contents()->SetLayoutManager(std::make_unique<views::BoxLayout>( |
| views::BoxLayout::Orientation::kVertical)); |
| |
| capture_mode_util::SetHighlightBorder( |
| this, kCornerRadius, |
| views::HighlightBorder::Type::kHighlightBorderOnShadow); |
| |
| shadow_->SetRoundedCornerRadius(kCornerRadius); |
| } |
| |
| CaptureModeSettingsView::~CaptureModeSettingsView() { |
| CaptureModeController::Get()->camera_controller()->RemoveObserver(this); |
| } |
| |
| // static |
| gfx::Rect CaptureModeSettingsView::GetBounds( |
| CaptureModeBarView* capture_mode_bar_view, |
| CaptureModeSettingsView* settings_view) { |
| DCHECK(capture_mode_bar_view); |
| |
| const gfx::Size settings_size = |
| settings_view ? settings_view->GetPreferredSize() : kSettingsSize; |
| return GetWidgetBounds(capture_mode_bar_view, settings_size); |
| } |
| |
| void CaptureModeSettingsView::OnCaptureFolderMayHaveChanged() { |
| if (!save_to_menu_group_) |
| return; |
| auto* controller = CaptureModeController::Get(); |
| const auto custom_path = controller->GetCustomCaptureFolder(); |
| if (custom_path.empty()) { |
| is_custom_folder_available_.reset(); |
| save_to_menu_group_->RemoveOptionIfAny(kCustomFolder); |
| save_to_menu_group_->RefreshOptionsSelections(); |
| return; |
| } |
| |
| std::u16string folder_name = custom_path.BaseName().AsUTF16Unsafe(); |
| // We explicitly name the folders of Google Drive and Play files, since those |
| // folders internally may have user-unfriendly names. |
| if (controller->IsRootDriveFsPath(custom_path)) { |
| folder_name = |
| l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_SAVE_TO_GOOGLE_DRIVE); |
| } else if (controller->IsAndroidFilesPath(custom_path)) { |
| folder_name = |
| l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_SAVE_TO_ANDROID_FILES); |
| } else if (controller->IsLinuxFilesPath(custom_path)) { |
| folder_name = |
| l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_SAVE_TO_LINUX_FILES); |
| } else if (controller->IsRootOneDriveFilesPath(custom_path)) { |
| folder_name = |
| l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_SAVE_TO_ONE_DRIVE); |
| } |
| |
| save_to_menu_group_->AddOrUpdateExistingOption( |
| /*option_icon=*/nullptr, folder_name, kCustomFolder); |
| |
| controller->CheckFolderAvailability( |
| custom_path, |
| base::BindOnce( |
| &CaptureModeSettingsView::OnCustomFolderAvailabilityChecked, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void CaptureModeSettingsView::OnDefaultCaptureFolderSelectionChanged() { |
| if (save_to_menu_group_) |
| save_to_menu_group_->RefreshOptionsSelections(); |
| } |
| |
| std::vector<CaptureModeSessionFocusCycler::HighlightableView*> |
| CaptureModeSettingsView::GetHighlightableItems() { |
| std::vector<CaptureModeSessionFocusCycler::HighlightableView*> |
| highlightable_items; |
| DCHECK(audio_input_menu_group_); |
| audio_input_menu_group_->AppendHighlightableItems(highlightable_items); |
| DCHECK(camera_menu_group_); |
| camera_menu_group_->AppendHighlightableItems(highlightable_items); |
| |
| if (demo_tools_menu_toggle_button_) { |
| highlightable_items.push_back( |
| CaptureModeSessionFocusCycler::HighlightHelper::Get( |
| demo_tools_menu_toggle_button_->toggle_button())); |
| } |
| |
| if (save_to_menu_group_) { |
| save_to_menu_group_->AppendHighlightableItems(highlightable_items); |
| } |
| |
| return highlightable_items; |
| } |
| |
| void CaptureModeSettingsView::OnOptionSelected(int option_id) const { |
| auto* controller = CaptureModeController::Get(); |
| auto* camera_controller = controller->camera_controller(); |
| switch (option_id) { |
| case kAudioOff: |
| controller->SetAudioRecordingMode(AudioRecordingMode::kOff); |
| break; |
| case kAudioSystem: |
| controller->SetAudioRecordingMode(AudioRecordingMode::kSystem); |
| break; |
| case kAudioMicrophone: |
| controller->SetAudioRecordingMode(AudioRecordingMode::kMicrophone); |
| break; |
| case kAudioSystemAndMicrophone: |
| controller->SetAudioRecordingMode( |
| AudioRecordingMode::kSystemAndMicrophone); |
| break; |
| case kDownloadsFolder: |
| controller->SetUsesDefaultCaptureFolder(true); |
| RecordSwitchToDefaultFolderReason( |
| CaptureModeSwitchToDefaultReason::kUserSelectedFromSettingsMenu); |
| break; |
| case kCustomFolder: |
| controller->SetUsesDefaultCaptureFolder(false); |
| break; |
| case kCameraOff: |
| camera_controller->SetSelectedCamera(CameraId()); |
| break; |
| default: |
| DCHECK(!camera_controller->IsCameraDisabledByPolicy()); |
| DCHECK_GE(option_id, kCameraDevicesBegin); |
| const CameraId* camera_id = FindCameraIdByOptionId(option_id); |
| DCHECK(camera_id); |
| camera_controller->SetSelectedCamera(*camera_id); |
| break; |
| } |
| } |
| |
| bool CaptureModeSettingsView::IsOptionChecked(int option_id) const { |
| auto* controller = CaptureModeController::Get(); |
| auto* camera_controller = controller->camera_controller(); |
| const auto effective_audio_mode = |
| controller->GetEffectiveAudioRecordingMode(); |
| switch (option_id) { |
| case kAudioOff: |
| return effective_audio_mode == AudioRecordingMode::kOff; |
| case kAudioSystem: |
| return effective_audio_mode == AudioRecordingMode::kSystem; |
| case kAudioMicrophone: |
| return effective_audio_mode == AudioRecordingMode::kMicrophone; |
| case kAudioSystemAndMicrophone: |
| return effective_audio_mode == AudioRecordingMode::kSystemAndMicrophone; |
| case kDownloadsFolder: |
| return GetCurrentCaptureFolder().is_default_downloads_folder || |
| !is_custom_folder_available_.value_or(false); |
| case kCustomFolder: |
| return !GetCurrentCaptureFolder().is_default_downloads_folder && |
| is_custom_folder_available_.value_or(false); |
| case kCameraOff: |
| return !camera_controller->selected_camera().is_valid(); |
| default: |
| DCHECK(!camera_controller->IsCameraDisabledByPolicy()); |
| DCHECK_GE(option_id, kCameraDevicesBegin); |
| const CameraId* camera_id = FindCameraIdByOptionId(option_id); |
| DCHECK(camera_id); |
| return *camera_id == camera_controller->selected_camera(); |
| } |
| } |
| |
| bool CaptureModeSettingsView::IsOptionEnabled(int option_id) const { |
| auto* controller = CaptureModeController::Get(); |
| const bool audio_capture_managed_by_policy = |
| controller->IsAudioCaptureDisabledByPolicy(); |
| switch (option_id) { |
| case kAudioOff: |
| return !audio_capture_managed_by_policy && |
| active_behavior_->SupportsAudioRecordingMode( |
| AudioRecordingMode::kOff); |
| case kAudioSystem: |
| case kAudioMicrophone: |
| case kAudioSystemAndMicrophone: |
| return !audio_capture_managed_by_policy; |
| case kCustomFolder: |
| return is_custom_folder_available_.value_or(false); |
| case kCameraOff: { |
| auto* camera_controller = controller->camera_controller(); |
| DCHECK(camera_controller); |
| return !camera_controller->IsCameraDisabledByPolicy(); |
| } |
| case kDownloadsFolder: |
| return !controller->IsCustomFolderManagedByPolicy(); |
| default: |
| return true; |
| } |
| } |
| |
| void CaptureModeSettingsView::OnAvailableCamerasChanged( |
| const CameraInfoList& cameras) { |
| auto* controller = CaptureModeController::Get(); |
| DCHECK(!controller->is_recording_in_progress()); |
| DCHECK(camera_menu_group_); |
| auto* camera_controller = controller->camera_controller(); |
| DCHECK(camera_controller); |
| AddCameraOptions(cameras, camera_controller->IsCameraDisabledByPolicy()); |
| |
| // If the size of the given `cameras` is equal to the size of the current |
| // available cameras, the bounds of the `camera_menu_group_` won't be updated, |
| // hence a layout may not be triggered. This can cause the newly added camera |
| // options to be not visible. We must guarantee that a layout will always |
| // occur by invalidating the layout. |
| camera_menu_group_->InvalidateLayout(); |
| camera_menu_group_->RefreshOptionsSelections(); |
| capture_mode_session_->MaybeUpdateSettingsBounds(); |
| } |
| |
| void CaptureModeSettingsView::OnSelectedCameraChanged( |
| const CameraId& camera_id) { |
| // TODO(conniekxu): Implement this function. |
| } |
| |
| void CaptureModeSettingsView::OnSelectFolderMenuItemPressed() { |
| capture_mode_session_->OpenFolderSelectionDialog(); |
| } |
| |
| void CaptureModeSettingsView::OnCustomFolderAvailabilityChecked( |
| bool available) { |
| DCHECK(save_to_menu_group_); |
| is_custom_folder_available_ = available; |
| save_to_menu_group_->RefreshOptionsSelections(); |
| if (!is_custom_folder_available_.value_or(false)) { |
| RecordSwitchToDefaultFolderReason( |
| CaptureModeSwitchToDefaultReason::kFolderUnavailable); |
| } |
| if (on_settings_menu_refreshed_callback_for_test_) |
| std::move(on_settings_menu_refreshed_callback_for_test_).Run(); |
| } |
| |
| const CameraId* CaptureModeSettingsView::FindCameraIdByOptionId( |
| int option_id) const { |
| auto target_it = option_camera_id_map_.find(option_id); |
| if (target_it != option_camera_id_map_.end()) |
| return &(target_it->second); |
| return nullptr; |
| } |
| |
| void CaptureModeSettingsView::AddCameraOptions(const CameraInfoList& cameras, |
| bool managed_by_policy) { |
| DCHECK(camera_menu_group_); |
| camera_menu_group_->DeleteOptions(); |
| option_camera_id_map_.clear(); |
| const bool has_cameras = !cameras.empty(); |
| if (has_cameras) { |
| camera_menu_group_->AddOption( |
| /*option_icon=*/nullptr, |
| l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_CAMERA_OFF), |
| kCameraOff); |
| if (!managed_by_policy) { |
| int camera_option_id_begin = kCameraDevicesBegin; |
| for (const CameraInfo& camera_info : cameras) { |
| option_camera_id_map_[camera_option_id_begin] = camera_info.camera_id; |
| camera_menu_group_->AddOption( |
| /*option_icon=*/nullptr, |
| base::UTF8ToUTF16(camera_info.display_name), |
| camera_option_id_begin++); |
| } |
| } |
| } |
| UpdateCameraMenuGroupVisibility(/*visible=*/has_cameras); |
| } |
| |
| void CaptureModeSettingsView::UpdateCameraMenuGroupVisibility(bool visible) { |
| separator_1_->SetVisible(visible); |
| camera_menu_group_->SetVisible(visible); |
| } |
| |
| void CaptureModeSettingsView::OnDemoToolsButtonToggled() { |
| const bool was_on = CaptureModeController::Get()->enable_demo_tools(); |
| CaptureModeController::Get()->EnableDemoTools(/*enable=*/!was_on); |
| } |
| |
| BEGIN_METADATA(CaptureModeSettingsView) |
| END_METADATA |
| |
| } // namespace ash |