| // Copyright 2020 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/clipboard/clipboard_history_util.h" |
| |
| #include <array> |
| #include <string_view> |
| |
| #include "ash/clipboard/clipboard_history_item.h" |
| #include "ash/clipboard/views/clipboard_history_view_constants.h" |
| #include "ash/metrics/histogram_macros.h" |
| #include "ash/public/cpp/assistant/assistant_state.h" |
| #include "ash/resources/vector_icons/vector_icons.h" |
| #include "ash/session/session_controller_impl.h" |
| #include "ash/shell.h" |
| #include "ash/strings/grit/ash_strings.h" |
| #include "base/files/file_path.h" |
| #include "base/no_destructor.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "cc/paint/paint_flags.h" |
| #include "chromeos/constants/chromeos_features.h" |
| #include "chromeos/crosapi/mojom/clipboard_history.mojom.h" |
| #include "chromeos/ui/base/file_icon_util.h" |
| #include "ui/base/clipboard/clipboard_data.h" |
| #include "ui/base/clipboard/custom_data_helper.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/models/image_model.h" |
| #include "ui/chromeos/styles/cros_tokens_color_mappings.h" |
| #include "ui/events/ash/keyboard_capability.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/image/canvas_image_source.h" |
| #include "ui/gfx/paint_vector_icon.h" |
| #include "ui/views/controls/menu/menu_config.h" |
| |
| namespace ash::clipboard_history_util { |
| |
| namespace { |
| |
| // Constants ------------------------------------------------------------------- |
| |
| constexpr char16_t kFileSystemSourcesType[] = u"fs/sources"; |
| |
| constexpr int kPlaceholderImageWidth = 234; |
| constexpr int kPlaceholderImageHeight = 74; |
| constexpr int kPlaceholderImageOutlineCornerRadius = 8; |
| |
| // The array of formats in order of decreasing priority. |
| constexpr ui::ClipboardInternalFormat kPrioritizedFormats[] = { |
| ui::ClipboardInternalFormat::kPng, |
| ui::ClipboardInternalFormat::kHtml, |
| ui::ClipboardInternalFormat::kText, |
| ui::ClipboardInternalFormat::kRtf, |
| ui::ClipboardInternalFormat::kFilenames, |
| ui::ClipboardInternalFormat::kBookmark, |
| ui::ClipboardInternalFormat::kWeb, |
| ui::ClipboardInternalFormat::kCustom}; |
| |
| // The clipboard history menu's width, in pixels. |
| constexpr int kPreferredMenuWidth = 320; |
| |
| // Helper classes -------------------------------------------------------------- |
| |
| // Used to draw a placeholder HTML preview to be shown while the real HTML is |
| // rendering. |
| class UnrenderedHtmlPlaceholderImage : public gfx::CanvasImageSource { |
| public: |
| UnrenderedHtmlPlaceholderImage() |
| : gfx::CanvasImageSource( |
| gfx::Size(kPlaceholderImageWidth, kPlaceholderImageHeight)) {} |
| UnrenderedHtmlPlaceholderImage(const UnrenderedHtmlPlaceholderImage&) = |
| delete; |
| UnrenderedHtmlPlaceholderImage& operator=( |
| const UnrenderedHtmlPlaceholderImage&) = delete; |
| ~UnrenderedHtmlPlaceholderImage() override = default; |
| |
| // gfx::CanvasImageSource: |
| void Draw(gfx::Canvas* canvas) override { |
| cc::PaintFlags flags; |
| flags.setStyle(cc::PaintFlags::kFill_Style); |
| flags.setAntiAlias(true); |
| flags.setColor(gfx::kGoogleGrey100); |
| canvas->DrawRoundRect( |
| /*rect=*/{kPlaceholderImageWidth, kPlaceholderImageHeight}, |
| kPlaceholderImageOutlineCornerRadius, flags); |
| |
| flags = cc::PaintFlags(); |
| flags.setStyle(cc::PaintFlags::kFill_Style); |
| flags.setAntiAlias(true); |
| const gfx::ImageSkia center_image = gfx::CreateVectorIcon( |
| kUnrenderedHtmlPlaceholderIcon, |
| ClipboardHistoryViews::kBitmapItemPlaceholderIconSize, |
| gfx::kGoogleGrey600); |
| canvas->DrawImageInt( |
| center_image, (size().width() - center_image.size().width()) / 2, |
| (size().height() - center_image.size().height()) / 2, flags); |
| } |
| }; |
| |
| } // namespace |
| |
| std::optional<ui::ClipboardInternalFormat> CalculateMainFormat( |
| const ui::ClipboardData& data) { |
| for (const auto& format : kPrioritizedFormats) { |
| if (ContainsFormat(data, format)) { |
| return format; |
| } |
| } |
| return std::nullopt; |
| } |
| |
| bool ContainsFormat(const ui::ClipboardData& data, |
| ui::ClipboardInternalFormat format) { |
| return data.format() & static_cast<int>(format); |
| } |
| |
| void RecordClipboardHistoryItemDeleted(const ClipboardHistoryItem& item) { |
| UMA_HISTOGRAM_ENUMERATION( |
| "Ash.ClipboardHistory.ContextMenu.DisplayFormatDeleted", |
| item.display_format()); |
| } |
| |
| void RecordClipboardHistoryItemPasted(const ClipboardHistoryItem& item) { |
| UMA_HISTOGRAM_ENUMERATION( |
| "Ash.ClipboardHistory.ContextMenu.DisplayFormatPasted", |
| item.display_format()); |
| } |
| |
| bool ContainsFileSystemData(const ui::ClipboardData& data) { |
| return !GetFileSystemSources(data).empty(); |
| } |
| |
| void GetSplitFileSystemData(const ui::ClipboardData& data, |
| std::vector<std::u16string_view>* source_list, |
| std::u16string* sources) { |
| DCHECK(sources); |
| DCHECK(sources->empty()); |
| DCHECK(source_list); |
| DCHECK(source_list->empty()); |
| |
| *sources = GetFileSystemSources(data); |
| if (sources->empty()) { |
| // Not a file system data. |
| return; |
| } |
| |
| // Split sources into a list. |
| *source_list = base::SplitStringPiece(*sources, u"\n", base::TRIM_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY); |
| } |
| |
| size_t GetCountOfCopiedFiles(const ui::ClipboardData& data) { |
| std::u16string sources; |
| std::vector<std::u16string_view> source_list; |
| GetSplitFileSystemData(data, &source_list, &sources); |
| |
| if (sources.empty()) { |
| // Not a file system data. |
| return 0; |
| } |
| |
| return source_list.size(); |
| } |
| |
| std::u16string GetFileSystemSources(const ui::ClipboardData& data) { |
| // Outside of the Files app, file system sources are written as filenames. |
| if (ContainsFormat(data, ui::ClipboardInternalFormat::kFilenames)) { |
| std::vector<std::string> sources; |
| for (const ui::FileInfo& filename : data.filenames()) { |
| sources.push_back(filename.path.value()); |
| } |
| return base::UTF8ToUTF16(base::JoinString(sources, "\n")); |
| } |
| |
| // Within the Files app, file system sources are written as custom data. |
| if (!ContainsFormat(data, ui::ClipboardInternalFormat::kCustom)) |
| return std::u16string(); |
| |
| // Attempt to read file system sources in the custom data. |
| if (std::optional<std::u16string> maybe_sources = ui::ReadCustomDataForType( |
| base::as_bytes(base::span(data.GetWebCustomData())), |
| kFileSystemSourcesType); |
| maybe_sources) { |
| return std::move(*maybe_sources); |
| } |
| |
| return std::u16string(); |
| } |
| |
| const gfx::VectorIcon& GetShortcutKeyIcon() { |
| if (!Shell::Get()->keyboard_capability()->HasLauncherButtonOnAnyKeyboard()) { |
| return kClipboardSearchIcon; |
| } |
| |
| const auto* const assistant_state = AssistantState::Get(); |
| const bool is_assistant_available = |
| assistant_state && |
| assistant_state->allowed_state() == |
| assistant::AssistantAllowedState::ALLOWED && |
| assistant_state->settings_enabled().value_or(false); |
| |
| return is_assistant_available ? kClipboardLauncherIcon |
| : kClipboardLauncherNoAssistantIcon; |
| } |
| |
| std::u16string GetShortcutKeyName() { |
| return l10n_util::GetStringUTF16( |
| Shell::Get()->keyboard_capability()->HasLauncherButtonOnAnyKeyboard() |
| ? IDS_ASH_SHORTCUT_MODIFIER_LAUNCHER |
| : IDS_ASH_SHORTCUT_MODIFIER_SEARCH); |
| } |
| |
| bool IsSupported(const ui::ClipboardData& data) { |
| const std::optional<ui::ClipboardInternalFormat> format = |
| CalculateMainFormat(data); |
| |
| // Empty `data` is not supported. |
| if (!format.has_value()) |
| return false; |
| |
| // The only supported type of custom data is file system data. |
| if (format.value() == ui::ClipboardInternalFormat::kCustom) |
| return ContainsFileSystemData(data); |
| |
| return true; |
| } |
| |
| bool IsEnabledInCurrentMode() { |
| const auto* session_controller = Shell::Get()->session_controller(); |
| |
| // The clipboard history menu is enabled only when a user has logged in and |
| // login UI is hidden. |
| if (session_controller->GetSessionState() != |
| session_manager::SessionState::ACTIVE) { |
| return false; |
| } |
| |
| switch (session_controller->login_status()) { |
| case LoginStatus::NOT_LOGGED_IN: |
| case LoginStatus::LOCKED: |
| case LoginStatus::KIOSK_APP: |
| case LoginStatus::PUBLIC: |
| return false; |
| case LoginStatus::USER: |
| case LoginStatus::GUEST: |
| case LoginStatus::CHILD: |
| return true; |
| } |
| } |
| |
| ui::ImageModel GetIconForFileClipboardItem(const ClipboardHistoryItem& item) { |
| DCHECK_EQ(item.display_format(), |
| crosapi::mojom::ClipboardHistoryDisplayFormat::kFile); |
| const int copied_files_count = GetCountOfCopiedFiles(item.data()); |
| if (copied_files_count == 0) |
| return ui::ImageModel(); |
| |
| constexpr std::array<const gfx::VectorIcon*, 9> icons = { |
| &kTwoFilesIcon, &kThreeFilesIcon, &kFourFilesIcon, |
| &kFiveFilesIcon, &kSixFilesIcon, &kSevenFilesIcon, |
| &kEightFilesIcon, &kNineFilesIcon, &kMoreThanNineFilesIcon}; |
| int icon_index = std::min(copied_files_count - 2, (int)icons.size() - 1); |
| |
| const auto* vector_icon = copied_files_count == 1 |
| ? &chromeos::GetIconForPath(base::FilePath( |
| base::UTF16ToUTF8(item.display_text()))) |
| : icons[icon_index]; |
| return ui::ImageModel::FromVectorIcon(*vector_icon, ui::kColorSysSecondary); |
| } |
| |
| ui::ImageModel GetHtmlPreviewPlaceholder() { |
| static base::NoDestructor<ui::ImageModel> model( |
| chromeos::features::IsClipboardHistoryRefreshEnabled() |
| ? ui::ImageModel::FromVectorIcon( |
| kUnrenderedHtmlPlaceholderIcon, cros_tokens::kCrosSysOutline, |
| ClipboardHistoryViews::kBitmapItemPlaceholderIconSize) |
| : ui::ImageModel::FromImageSkia(gfx::CanvasImageSource::MakeImageSkia< |
| UnrenderedHtmlPlaceholderImage>())); |
| return *model; |
| } |
| |
| crosapi::mojom::ClipboardHistoryItemDescriptor ItemToDescriptor( |
| const ClipboardHistoryItem& item) { |
| return crosapi::mojom::ClipboardHistoryItemDescriptor( |
| item.id(), item.display_format(), item.display_text(), item.file_count()); |
| } |
| |
| int GetPreferredItemViewWidth() { |
| const auto& menu_config = views::MenuConfig::instance(); |
| return chromeos::features::IsClipboardHistoryRefreshEnabled() |
| ? std::clamp(kPreferredMenuWidth, |
| menu_config.touchable_menu_min_width, |
| menu_config.touchable_menu_max_width) |
| : menu_config.touchable_menu_min_width; |
| } |
| |
| } // namespace ash::clipboard_history_util |