multipaste: Show educational footer when Ctrl+V long-press shows menu
This CL adds a non-interactive item at the end of the clipboard history context menu when the menu is shown via the experimental Ctrl+V long-press shortcut. This footer contains a message explaining to users why the menu has appeared and how they can disable this behavior to re-gain the keyboard auto-repeat behavior it overrides. Screenshot: https://screenshot.googleplex.com/B2d6hCfhnn9EhTG Note that this change makes no explicit checks for the clipboard history long-press feature flag, because that check is made before the menu can be shown with the Ctrl+V long-press show source. Bug: b/267694412, b/267680961 Change-Id: Ibe2e8c347a1cc3bae7ae45f7c10be7f2fc2e5220 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4456521 Commit-Queue: Colin Kincaid <ckincaid@chromium.org> Reviewed-by: David Black <dmblack@google.com> Cr-Commit-Position: refs/heads/main@{#1146729}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
ba3ee2d049
commit
e44977c6d0
@ -3004,6 +3004,7 @@ test("ash_unittests") {
|
||||
"child_accounts/parent_access_controller_impl_unittest.cc",
|
||||
"clipboard/clipboard_history_controller_unittest.cc",
|
||||
"clipboard/clipboard_history_item_unittest.cc",
|
||||
"clipboard/clipboard_history_menu_model_adapter_unittest.cc",
|
||||
"clipboard/clipboard_history_resource_manager_unittest.cc",
|
||||
"clipboard/clipboard_history_unittest.cc",
|
||||
"clipboard/clipboard_history_util_unittest.cc",
|
||||
|
@ -5843,6 +5843,9 @@ Here are some things you can try to get started.
|
||||
</message>
|
||||
|
||||
<!-- Multipaste -->
|
||||
<message name="IDS_ASH_CLIPBOARD_HISTORY_CONTROL_V_LONGPRESS_FOOTER" desc="The message used to educate users about the clipboard history menu that shows when they hold down Ctrl+V.">
|
||||
You’ll see the clipboard when you press and hold Ctrl + V. You can turn off this shortcut by disabling the #clipboard-history-longpress flag in chrome://flags (os://flags if using Lacros).
|
||||
</message>
|
||||
<message name="IDS_ASH_MULTIPASTE_CONTEXTUAL_NUDGE" desc="The label used for the multipaste nudge, to be seen once the user copies and pastes multiple times in a short time span.">
|
||||
Press <ph name="SHORTCUT_KEY_NAME">$1<ex>Launcher</ex></ph> + V to view your clipboard. The last 5 items you've copied are saved to your clipboard.
|
||||
</message>
|
||||
|
@ -0,0 +1 @@
|
||||
a0e4cd5c7fddca027bc99748023f40a175549a36
|
@ -430,7 +430,7 @@ bool ClipboardHistoryControllerImpl::ShowMenu(
|
||||
base::BindRepeating(&ClipboardHistoryControllerImpl::OnMenuClosed,
|
||||
base::Unretained(this)),
|
||||
clipboard_history_.get());
|
||||
context_menu_->Run(anchor_rect, source_type);
|
||||
context_menu_->Run(anchor_rect, source_type, show_source);
|
||||
|
||||
DCHECK(IsMenuShowing());
|
||||
accelerator_target_->OnMenuShown();
|
||||
|
@ -4,32 +4,86 @@
|
||||
|
||||
#include "ash/clipboard/clipboard_history_menu_model_adapter.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "ash/bubble/bubble_utils.h"
|
||||
#include "ash/clipboard/clipboard_history.h"
|
||||
#include "ash/clipboard/clipboard_history_util.h"
|
||||
#include "ash/clipboard/views/clipboard_history_item_view.h"
|
||||
#include "ash/clipboard/views/clipboard_history_label.h"
|
||||
#include "ash/clipboard/views/clipboard_history_view_constants.h"
|
||||
#include "ash/constants/ash_features.h"
|
||||
#include "ash/public/cpp/clipboard_history_controller.h"
|
||||
#include "ash/public/cpp/clipboard_image_model_factory.h"
|
||||
#include "ash/strings/grit/ash_strings.h"
|
||||
#include "ash/style/typography.h"
|
||||
#include "ash/wm/window_util.h"
|
||||
#include "base/check.h"
|
||||
#include "base/functional/bind.h"
|
||||
#include "base/memory/raw_ptr.h"
|
||||
#include "base/metrics/histogram_macros.h"
|
||||
#include "base/ranges/algorithm.h"
|
||||
#include "base/strings/string_util.h"
|
||||
#include "base/task/sequenced_task_runner.h"
|
||||
#include "ui/accessibility/ax_enums.mojom.h"
|
||||
#include "ui/base/l10n/l10n_util.h"
|
||||
#include "ui/base/models/menu_model.h"
|
||||
#include "ui/base/models/simple_menu_model.h"
|
||||
#include "ui/base/ui_base_types.h"
|
||||
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
|
||||
#include "ui/gfx/geometry/rect.h"
|
||||
#include "ui/strings/grit/ui_strings.h"
|
||||
#include "ui/views/accessibility/view_accessibility.h"
|
||||
#include "ui/views/border.h"
|
||||
#include "ui/views/controls/menu/menu_item_view.h"
|
||||
#include "ui/views/controls/menu/menu_model_adapter.h"
|
||||
#include "ui/views/controls/menu/menu_runner.h"
|
||||
#include "ui/views/controls/menu/menu_types.h"
|
||||
#include "ui/views/controls/menu/submenu_view.h"
|
||||
#include "ui/views/controls/separator.h"
|
||||
#include "ui/views/layout/box_layout.h"
|
||||
#include "ui/views/layout/box_layout_view.h"
|
||||
#include "ui/views/widget/widget.h"
|
||||
|
||||
namespace ash {
|
||||
|
||||
namespace {
|
||||
|
||||
// Populates `container` with a separator and a label containing educational
|
||||
// content to appear at the bottom of the clipboard history menu.
|
||||
void InsertFooterContent(views::MenuItemView* container) {
|
||||
const int content_width =
|
||||
clipboard_history_util::GetPreferredItemViewWidth() -
|
||||
ClipboardHistoryViews::kContentsInsets.width();
|
||||
|
||||
// Introduce a layout view between `container` and the desired separator and
|
||||
// label to circumvent `container` manually laying out its children.
|
||||
container->AddChildView(
|
||||
views::Builder<views::BoxLayoutView>()
|
||||
.SetOrientation(views::BoxLayout::Orientation::kVertical)
|
||||
.AddChildren(
|
||||
views::Builder<views::Separator>()
|
||||
.SetBorder(views::CreateEmptyBorder(
|
||||
ClipboardHistoryViews::kContentsInsets))
|
||||
.SetColorId(cros_tokens::kCrosSysSeparator)
|
||||
.SetOrientation(views::Separator::Orientation::kHorizontal)
|
||||
.SetPreferredLength(content_width),
|
||||
views::Builder<views::Label>(
|
||||
bubble_utils::CreateLabel(
|
||||
TypographyToken::kCrosAnnotation1,
|
||||
l10n_util::GetStringUTF16(
|
||||
IDS_ASH_CLIPBOARD_HISTORY_CONTROL_V_LONGPRESS_FOOTER),
|
||||
cros_tokens::kCrosSysSecondary))
|
||||
.SetBorder(views::CreateEmptyBorder(
|
||||
ClipboardHistoryViews::kContentsInsets))
|
||||
.SetHorizontalAlignment(gfx::ALIGN_LEFT)
|
||||
.SetMultiLine(true)
|
||||
.SizeToFit(/*fixed_width=*/content_width))
|
||||
.Build());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ClipboardHistoryMenuModelAdapter::MenuModelWithWillCloseCallback ------------
|
||||
|
||||
// Utility class that allows `ClipboardHistoryMenuModelAdapter` to run a task
|
||||
@ -104,7 +158,8 @@ ClipboardHistoryMenuModelAdapter::~ClipboardHistoryMenuModelAdapter() = default;
|
||||
|
||||
void ClipboardHistoryMenuModelAdapter::Run(
|
||||
const gfx::Rect& anchor_rect,
|
||||
ui::MenuSourceType source_type) {
|
||||
ui::MenuSourceType source_type,
|
||||
crosapi::mojom::ClipboardHistoryControllerShowSource show_source) {
|
||||
DCHECK(!root_view_);
|
||||
DCHECK(model_);
|
||||
DCHECK(item_snapshots_.empty());
|
||||
@ -131,6 +186,14 @@ void ClipboardHistoryMenuModelAdapter::Run(
|
||||
++command_id;
|
||||
}
|
||||
|
||||
if (show_source == crosapi::mojom::ClipboardHistoryControllerShowSource::
|
||||
kControlVLongpress) {
|
||||
// Add placeholder non-interactive item that will contain a separator
|
||||
// (styled differently from the context menu separators) and educational
|
||||
// footer text.
|
||||
model_->AddTitle(std::u16string());
|
||||
}
|
||||
|
||||
// Start async rendering of HTML, if any exists.
|
||||
// This factory may be nullptr in tests.
|
||||
if (auto* clipboard_image_factory = ClipboardImageModelFactory::Get()) {
|
||||
@ -145,7 +208,7 @@ void ClipboardHistoryMenuModelAdapter::Run(
|
||||
views::MenuRunner::USE_ASH_SYS_UI_LAYOUT |
|
||||
views::MenuRunner::FIXED_ANCHOR);
|
||||
menu_runner_->RunMenuAt(
|
||||
/*widget_owner=*/nullptr, /*menu_button_controller=*/nullptr, anchor_rect,
|
||||
/*parent=*/nullptr, /*button_controller=*/nullptr, anchor_rect,
|
||||
views::MenuAnchorPosition::kBubbleBottomRight, source_type);
|
||||
}
|
||||
|
||||
@ -321,6 +384,11 @@ views::MenuItemView* ClipboardHistoryMenuModelAdapter::GetMenuItemViewAtForTest(
|
||||
->GetMenuItemViewAtForTest(index));
|
||||
}
|
||||
|
||||
const ui::SimpleMenuModel* ClipboardHistoryMenuModelAdapter::GetModelForTest()
|
||||
const {
|
||||
return model_.get();
|
||||
}
|
||||
|
||||
ClipboardHistoryMenuModelAdapter::ClipboardHistoryMenuModelAdapter(
|
||||
std::unique_ptr<MenuModelWithWillCloseCallback> model,
|
||||
base::RepeatingClosure menu_closed_callback,
|
||||
@ -430,12 +498,21 @@ views::MenuItemView* ClipboardHistoryMenuModelAdapter::AppendMenuItem(
|
||||
// Margins are managed by `ClipboardHistoryItemView`.
|
||||
container->SetMargins(/*top_margin=*/0, /*bottom_margin=*/0);
|
||||
|
||||
std::unique_ptr<ClipboardHistoryItemView> item_view =
|
||||
ClipboardHistoryItemView::CreateFromClipboardHistoryItem(
|
||||
GetItemFromCommandId(command_id).id(), clipboard_history_, container);
|
||||
item_view->Init();
|
||||
item_views_by_command_id_.insert(std::make_pair(command_id, item_view.get()));
|
||||
container->AddChildView(std::move(item_view));
|
||||
size_t num_items = clipboard_history_->GetItems().size();
|
||||
if (index < num_items) {
|
||||
std::unique_ptr<ClipboardHistoryItemView> item_view =
|
||||
ClipboardHistoryItemView::CreateFromClipboardHistoryItem(
|
||||
GetItemFromCommandId(command_id).id(), clipboard_history_,
|
||||
container);
|
||||
item_view->Init();
|
||||
item_views_by_command_id_.insert(
|
||||
std::make_pair(command_id, item_view.get()));
|
||||
container->AddChildView(std::move(item_view));
|
||||
} else {
|
||||
CHECK_EQ(index, num_items);
|
||||
CHECK_EQ(model->GetTypeAt(index), ui::MenuModel::ItemType::TYPE_TITLE);
|
||||
InsertFooterContent(container);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
@ -11,6 +11,7 @@
|
||||
#include "ash/public/cpp/clipboard_history_controller.h"
|
||||
#include "base/memory/raw_ptr.h"
|
||||
#include "base/time/time.h"
|
||||
#include "chromeos/crosapi/mojom/clipboard_history.mojom.h"
|
||||
#include "third_party/abseil-cpp/absl/types/optional.h"
|
||||
#include "ui/base/models/simple_menu_model.h"
|
||||
#include "ui/views/controls/menu/menu_model_adapter.h"
|
||||
@ -52,10 +53,11 @@ class ASH_EXPORT ClipboardHistoryMenuModelAdapter
|
||||
const ClipboardHistoryMenuModelAdapter&) = delete;
|
||||
~ClipboardHistoryMenuModelAdapter() override;
|
||||
|
||||
// Shows the menu anchored at `anchor_rect`, `source_type` indicates how the
|
||||
// menu is triggered.
|
||||
// Shows the menu anchored at `anchor_rect`. `source_type` and `show_source`
|
||||
// indicate how the menu was triggered.
|
||||
void Run(const gfx::Rect& anchor_rect,
|
||||
ui::MenuSourceType source_type);
|
||||
ui::MenuSourceType source_type,
|
||||
crosapi::mojom::ClipboardHistoryControllerShowSource show_source);
|
||||
|
||||
// Returns if the menu is currently running.
|
||||
bool IsRunning() const;
|
||||
@ -95,6 +97,8 @@ class ASH_EXPORT ClipboardHistoryMenuModelAdapter
|
||||
const views::MenuItemView* GetMenuItemViewAtForTest(size_t index) const;
|
||||
views::MenuItemView* GetMenuItemViewAtForTest(size_t index);
|
||||
|
||||
const ui::SimpleMenuModel* GetModelForTest() const;
|
||||
|
||||
private:
|
||||
class MenuModelWithWillCloseCallback;
|
||||
class ScopedA11yIgnore;
|
||||
|
@ -0,0 +1,98 @@
|
||||
// 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/clipboard/clipboard_history_menu_model_adapter.h"
|
||||
|
||||
#include "ash/clipboard/clipboard_history.h"
|
||||
#include "ash/clipboard/clipboard_history_controller_impl.h"
|
||||
#include "ash/shell.h"
|
||||
#include "ash/test/ash_test_base.h"
|
||||
#include "base/test/repeating_test_future.h"
|
||||
#include "chromeos/crosapi/mojom/clipboard_history.mojom.h"
|
||||
#include "testing/gtest/include/gtest/gtest.h"
|
||||
#include "ui/base/clipboard/scoped_clipboard_writer.h"
|
||||
#include "ui/base/models/simple_menu_model.h"
|
||||
|
||||
namespace ash {
|
||||
using crosapi::mojom::ClipboardHistoryControllerShowSource;
|
||||
|
||||
namespace {
|
||||
|
||||
ClipboardHistoryControllerImpl* GetClipboardHistoryController() {
|
||||
return Shell::Get()->clipboard_history_controller();
|
||||
}
|
||||
|
||||
std::vector<ClipboardHistoryControllerShowSource>
|
||||
GetClipboardHistoryShowSources() {
|
||||
std::vector<ClipboardHistoryControllerShowSource> sources;
|
||||
for (int i =
|
||||
static_cast<int>(ClipboardHistoryControllerShowSource::kMinValue);
|
||||
i <= static_cast<int>(ClipboardHistoryControllerShowSource::kMaxValue);
|
||||
++i) {
|
||||
sources.push_back(static_cast<ClipboardHistoryControllerShowSource>(i));
|
||||
}
|
||||
return sources;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Base class for `ClipboardHistoryMenuModelAdapter` tests that run with each
|
||||
// possible `ClipboardHistoryControllerShowSource`.
|
||||
class ClipboardHistoryMenuModelAdapterShowSourceTest
|
||||
: public AshTestBase,
|
||||
public testing::WithParamInterface<ClipboardHistoryControllerShowSource> {
|
||||
public:
|
||||
// AshTestBase:
|
||||
void SetUp() override {
|
||||
AshTestBase::SetUp();
|
||||
GetClipboardHistoryController()->set_confirmed_operation_callback_for_test(
|
||||
operation_confirmed_future_.GetCallback());
|
||||
}
|
||||
|
||||
void WriteTextToClipboardAndConfirm(const std::u16string& str) {
|
||||
{
|
||||
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
|
||||
scw.WriteText(str);
|
||||
}
|
||||
EXPECT_TRUE(operation_confirmed_future_.Take());
|
||||
}
|
||||
|
||||
ClipboardHistoryControllerShowSource GetSource() const { return GetParam(); }
|
||||
|
||||
private:
|
||||
base::test::RepeatingTestFuture<bool> operation_confirmed_future_;
|
||||
};
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(All,
|
||||
ClipboardHistoryMenuModelAdapterShowSourceTest,
|
||||
testing::ValuesIn(GetClipboardHistoryShowSources()));
|
||||
|
||||
// Verifies that the clipboard history menu has an educational footer iff it was
|
||||
// shown by the Ctrl+V long-press shortcut.
|
||||
TEST_P(ClipboardHistoryMenuModelAdapterShowSourceTest,
|
||||
ControlVLongpressShowsFooter) {
|
||||
// Write items to clipboard history so that the menu can show.
|
||||
WriteTextToClipboardAndConfirm(u"A");
|
||||
WriteTextToClipboardAndConfirm(u"B");
|
||||
|
||||
// Show the clipboard history menu.
|
||||
auto* controller = GetClipboardHistoryController();
|
||||
EXPECT_TRUE(controller->ShowMenu(
|
||||
gfx::Rect(), ui::MenuSourceType::MENU_SOURCE_NONE, GetSource()));
|
||||
EXPECT_TRUE(controller->IsMenuShowing());
|
||||
|
||||
// Verify that iff the menu was shown via Ctrl+V long-press, the menu has an
|
||||
// educational footer item; otherwise, the number of items in the menu should
|
||||
// match the number of items in clipboard history.
|
||||
const auto* model = controller->context_menu_for_test()->GetModelForTest();
|
||||
EXPECT_EQ(controller->history()->GetItems().size(), 2u);
|
||||
if (GetSource() == ClipboardHistoryControllerShowSource::kControlVLongpress) {
|
||||
ASSERT_EQ(model->GetItemCount(), 3u);
|
||||
EXPECT_EQ(model->GetTypeAt(2u), ui::MenuModel::ItemType::TYPE_TITLE);
|
||||
} else {
|
||||
EXPECT_EQ(model->GetItemCount(), 2u);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ash
|
@ -24,6 +24,7 @@
|
||||
#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 {
|
||||
|
||||
@ -250,4 +251,8 @@ GetItemDescriptorsFrom(const std::list<ClipboardHistoryItem>& items) {
|
||||
return item_descriptors;
|
||||
}
|
||||
|
||||
int GetPreferredItemViewWidth() {
|
||||
return views::MenuConfig::instance().touchable_menu_min_width;
|
||||
}
|
||||
|
||||
} // namespace ash::clipboard_history_util
|
||||
|
@ -161,6 +161,9 @@ ASH_EXPORT ui::ImageModel GetHtmlPreviewPlaceholder();
|
||||
std::vector<crosapi::mojom::ClipboardHistoryItemDescriptor>
|
||||
GetItemDescriptorsFrom(const std::list<ClipboardHistoryItem>& items);
|
||||
|
||||
// Calculates the preferred width for clipboard history menu item views.
|
||||
int GetPreferredItemViewWidth();
|
||||
|
||||
} // namespace clipboard_history_util
|
||||
} // namespace ash
|
||||
|
||||
|
@ -25,7 +25,6 @@
|
||||
#include "ui/views/accessibility/view_accessibility.h"
|
||||
#include "ui/views/animation/ink_drop.h"
|
||||
#include "ui/views/border.h"
|
||||
#include "ui/views/controls/menu/menu_config.h"
|
||||
#include "ui/views/controls/menu/menu_item_view.h"
|
||||
#include "ui/views/layout/fill_layout.h"
|
||||
|
||||
@ -270,7 +269,7 @@ const ClipboardHistoryItem* ClipboardHistoryItemView::GetClipboardHistoryItem()
|
||||
|
||||
gfx::Size ClipboardHistoryItemView::CalculatePreferredSize() const {
|
||||
const int preferred_width =
|
||||
views::MenuConfig::instance().touchable_menu_min_width;
|
||||
clipboard_history_util::GetPreferredItemViewWidth();
|
||||
return gfx::Size(preferred_width, GetHeightForWidth(preferred_width));
|
||||
}
|
||||
|
||||
|
@ -1503,6 +1503,55 @@ IN_PROC_BROWSER_TEST_F(ClipboardHistoryTextfieldBrowserTest,
|
||||
EXPECT_TRUE(textfield_->GetText().empty());
|
||||
}
|
||||
|
||||
// Verifies that clicking the clipboard history's menu does nothing and that tab
|
||||
// and arrow key traversal pass over the footer.
|
||||
IN_PROC_BROWSER_TEST_F(ClipboardHistoryTextfieldBrowserTest,
|
||||
FooterNotInteractive) {
|
||||
// Write some things to the clipboard.
|
||||
SetClipboardText("A");
|
||||
SetClipboardText("B");
|
||||
|
||||
// Show the clipboard history menu via the Ctrl+V long-press shortcut so that
|
||||
// the menu's educational footer shows.
|
||||
EXPECT_TRUE(GetClipboardHistoryController()->ShowMenu(
|
||||
gfx::Rect(), ui::MenuSourceType::MENU_SOURCE_NONE,
|
||||
crosapi::mojom::ClipboardHistoryControllerShowSource::
|
||||
kControlVLongpress));
|
||||
EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
|
||||
|
||||
// Verify that the menu has two clipboard history items and a third item (the
|
||||
// footer).
|
||||
const auto* menu = GetClipboardHistoryController()->context_menu_for_test();
|
||||
EXPECT_EQ(menu->GetMenuItemsCount(), 2u);
|
||||
ASSERT_EQ(menu->GetModelForTest()->GetItemCount(), 3u);
|
||||
|
||||
// Verify that clicking on the footer does nothing.
|
||||
EXPECT_TRUE(textfield_->GetText().empty());
|
||||
const auto* footer = menu->GetMenuItemViewAtForTest(/*index=*/2);
|
||||
GetEventGenerator()->MoveMouseTo(footer->GetBoundsInScreen().CenterPoint());
|
||||
GetEventGenerator()->ClickLeftButton();
|
||||
EXPECT_TRUE(textfield_->GetText().empty());
|
||||
|
||||
// Verify that traversing over the menu with arrow keys skips the footer.
|
||||
const auto* item1 = menu->GetMenuItemViewAtForTest(/*index=*/0);
|
||||
const auto* item2 = menu->GetMenuItemViewAtForTest(/*index=*/1);
|
||||
PressAndRelease(ui::VKEY_DOWN);
|
||||
EXPECT_TRUE(item1->IsSelected());
|
||||
PressAndRelease(ui::VKEY_DOWN);
|
||||
EXPECT_TRUE(item2->IsSelected());
|
||||
PressAndRelease(ui::VKEY_DOWN);
|
||||
EXPECT_TRUE(item1->IsSelected());
|
||||
|
||||
// Verify that traversing over the menu with the Tab key (two presses at a
|
||||
// time for each item's main button and delete button) skips the footer.
|
||||
PressAndRelease(ui::VKEY_TAB);
|
||||
PressAndRelease(ui::VKEY_TAB);
|
||||
EXPECT_TRUE(item2->IsSelected());
|
||||
PressAndRelease(ui::VKEY_TAB);
|
||||
PressAndRelease(ui::VKEY_TAB);
|
||||
EXPECT_TRUE(item1->IsSelected());
|
||||
}
|
||||
|
||||
class FakeDataTransferPolicyController
|
||||
: public ui::DataTransferPolicyController {
|
||||
public:
|
||||
|
Reference in New Issue
Block a user