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