1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "ui/views/touchui/touch_selection_menu_views.h"
6
7 #include <memory>
8 #include <utility>
9
10 #include "base/stl_util.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "ui/aura/window.h"
13 #include "ui/base/l10n/l10n_util.h"
14 #include "ui/base/pointer/touch_editing_controller.h"
15 #include "ui/display/display.h"
16 #include "ui/display/screen.h"
17 #include "ui/gfx/canvas.h"
18 #include "ui/gfx/geometry/insets.h"
19 #include "ui/gfx/geometry/rect.h"
20 #include "ui/gfx/geometry/size.h"
21 #include "ui/gfx/text_utils.h"
22 #include "ui/strings/grit/ui_strings.h"
23 #include "ui/touch_selection/touch_selection_menu_runner.h"
24 #include "ui/views/controls/button/label_button.h"
25 #include "ui/views/layout/box_layout.h"
26 #include "ui/views/metadata/metadata_impl_macros.h"
27
28 namespace views {
29 namespace {
30
31 struct MenuCommand {
32 int command_id;
33 int message_id;
34 } kMenuCommands[] = {
35 {ui::TouchEditable::kCut, IDS_APP_CUT},
36 {ui::TouchEditable::kCopy, IDS_APP_COPY},
37 {ui::TouchEditable::kPaste, IDS_APP_PASTE},
38 };
39
40 constexpr int kSpacingBetweenButtons = 2;
41
42 } // namespace
43
TouchSelectionMenuViews(TouchSelectionMenuRunnerViews * owner,ui::TouchSelectionMenuClient * client,aura::Window * context)44 TouchSelectionMenuViews::TouchSelectionMenuViews(
45 TouchSelectionMenuRunnerViews* owner,
46 ui::TouchSelectionMenuClient* client,
47 aura::Window* context)
48 : BubbleDialogDelegateView(nullptr, BubbleBorder::BOTTOM_CENTER),
49 owner_(owner),
50 client_(client) {
51 DCHECK(owner_);
52 DCHECK(client_);
53
54 DialogDelegate::SetButtons(ui::DIALOG_BUTTON_NONE);
55 set_shadow(BubbleBorder::SMALL_SHADOW);
56 set_parent_window(context);
57 constexpr gfx::Insets kMenuMargins = gfx::Insets(1);
58 set_margins(kMenuMargins);
59 SetCanActivate(false);
60 set_adjust_if_offscreen(true);
61 SetFlipCanvasOnPaintForRTLUI(true);
62
63 SetLayoutManager(
64 std::make_unique<BoxLayout>(BoxLayout::Orientation::kHorizontal,
65 gfx::Insets(), kSpacingBetweenButtons));
66 }
67
ShowMenu(const gfx::Rect & anchor_rect,const gfx::Size & handle_image_size)68 void TouchSelectionMenuViews::ShowMenu(const gfx::Rect& anchor_rect,
69 const gfx::Size& handle_image_size) {
70 CreateButtons();
71
72 // After buttons are created, check if there is enough room between handles to
73 // show the menu and adjust anchor rect properly if needed, just in case the
74 // menu is needed to be shown under the selection.
75 gfx::Rect adjusted_anchor_rect(anchor_rect);
76 int menu_width = GetPreferredSize().width();
77 // TODO(mfomitchev): This assumes that the handles are center-aligned to the
78 // |achor_rect| edges, which is not true. We should fix this, perhaps by
79 // passing down the cumulative width occupied by the handles within
80 // |anchor_rect| plus the handle image height instead of |handle_image_size|.
81 // Perhaps we should also allow for some minimum padding.
82 if (menu_width > anchor_rect.width() - handle_image_size.width())
83 adjusted_anchor_rect.Inset(0, 0, 0, -handle_image_size.height());
84 SetAnchorRect(adjusted_anchor_rect);
85
86 BubbleDialogDelegateView::CreateBubble(this);
87 Widget* widget = GetWidget();
88 gfx::Rect bounds = widget->GetWindowBoundsInScreen();
89 gfx::Rect work_area = display::Screen::GetScreen()
90 ->GetDisplayNearestPoint(bounds.origin())
91 .work_area();
92 if (!work_area.IsEmpty()) {
93 bounds.AdjustToFit(work_area);
94 widget->SetBounds(bounds);
95 }
96 // Using BubbleDialogDelegateView engages its CreateBubbleWidget() which
97 // invokes widget->StackAbove(context). That causes the bubble to stack
98 // _immediately_ above |context|; below any already-existing bubbles. That
99 // doesn't make sense for a menu, so put it back on top.
100 widget->StackAtTop();
101 widget->Show();
102 }
103
IsMenuAvailable(const ui::TouchSelectionMenuClient * client)104 bool TouchSelectionMenuViews::IsMenuAvailable(
105 const ui::TouchSelectionMenuClient* client) {
106 DCHECK(client);
107
108 const auto is_enabled = [client](MenuCommand command) {
109 return client->IsCommandIdEnabled(command.command_id);
110 };
111 return std::any_of(std::cbegin(kMenuCommands), std::cend(kMenuCommands),
112 is_enabled);
113 }
114
CloseMenu()115 void TouchSelectionMenuViews::CloseMenu() {
116 DisconnectOwner();
117 // Closing the widget will self-destroy this object.
118 Widget* widget = GetWidget();
119 if (widget && !widget->IsClosed())
120 widget->Close();
121 }
122
123 TouchSelectionMenuViews::~TouchSelectionMenuViews() = default;
124
CreateButtons()125 void TouchSelectionMenuViews::CreateButtons() {
126 for (const auto& command : kMenuCommands) {
127 if (client_->IsCommandIdEnabled(command.command_id)) {
128 CreateButton(
129 l10n_util::GetStringUTF16(command.message_id),
130 base::BindRepeating(&TouchSelectionMenuViews::ButtonPressed,
131 base::Unretained(this), command.command_id));
132 }
133 }
134
135 // Finally, add ellipsis button.
136 CreateButton(base::ASCIIToUTF16("..."),
137 base::BindRepeating(
138 [](TouchSelectionMenuViews* menu) {
139 menu->CloseMenu();
140 menu->client_->RunContextMenu();
141 },
142 base::Unretained(this)))
143 ->SetID(ButtonViewId::kEllipsisButton);
144 InvalidateLayout();
145 }
146
CreateButton(const base::string16 & title,Button::PressedCallback callback)147 LabelButton* TouchSelectionMenuViews::CreateButton(
148 const base::string16& title,
149 Button::PressedCallback callback) {
150 base::string16 label =
151 gfx::RemoveAcceleratorChar(title, '&', nullptr, nullptr);
152 auto* button = AddChildView(std::make_unique<LabelButton>(
153 std::move(callback), label, style::CONTEXT_TOUCH_MENU));
154 constexpr gfx::Size kMenuButtonMinSize = gfx::Size(63, 38);
155 button->SetMinSize(kMenuButtonMinSize);
156 button->SetHorizontalAlignment(gfx::ALIGN_CENTER);
157 return button;
158 }
159
DisconnectOwner()160 void TouchSelectionMenuViews::DisconnectOwner() {
161 DCHECK(owner_);
162 owner_->menu_ = nullptr;
163 owner_ = nullptr;
164 }
165
OnPaint(gfx::Canvas * canvas)166 void TouchSelectionMenuViews::OnPaint(gfx::Canvas* canvas) {
167 BubbleDialogDelegateView::OnPaint(canvas);
168 if (children().empty())
169 return;
170
171 // Draw separator bars.
172 for (auto i = children().cbegin(); i != std::prev(children().cend()); ++i) {
173 const View* child = *i;
174 int x = child->bounds().right() + kSpacingBetweenButtons / 2;
175 canvas->FillRect(gfx::Rect(x, 0, 1, child->height()),
176 GetNativeTheme()->GetSystemColor(
177 ui::NativeTheme::kColorId_SeparatorColor));
178 }
179 }
180
WindowClosing()181 void TouchSelectionMenuViews::WindowClosing() {
182 DCHECK(!owner_ || owner_->menu_ == this);
183 BubbleDialogDelegateView::WindowClosing();
184 if (owner_)
185 DisconnectOwner();
186 }
187
ButtonPressed(int command,const ui::Event & event)188 void TouchSelectionMenuViews::ButtonPressed(int command,
189 const ui::Event& event) {
190 CloseMenu();
191 client_->ExecuteCommand(command, event.flags());
192 }
193
194 BEGIN_METADATA(TouchSelectionMenuViews, BubbleDialogDelegateView)
195 END_METADATA
196
197 } // namespace views
198