| // Copyright 2015 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/views/touchui/touch_selection_menu_runner_views.h" |
| |
| #include <stddef.h> |
| |
| #include "base/macros.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "ui/aura/window.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/geometry/insets.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "ui/gfx/text_utils.h" |
| #include "ui/strings/grit/ui_strings.h" |
| #include "ui/views/bubble/bubble_dialog_delegate.h" |
| #include "ui/views/controls/button/button.h" |
| #include "ui/views/controls/button/label_button.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/style/typography.h" |
| |
| namespace views { |
| namespace { |
| |
| const int kMenuCommands[] = {IDS_APP_CUT, IDS_APP_COPY, IDS_APP_PASTE}; |
| const int kSpacingBetweenButtons = 2; |
| const int kButtonSeparatorColor = SkColorSetARGB(13, 0, 0, 0); |
| const int kMenuButtonMinHeight = 38; |
| const int kMenuButtonMinWidth = 63; |
| const int kMenuMargin = 1; |
| |
| const char kEllipsesButtonText[] = "..."; |
| const int kEllipsesButtonTag = -1; |
| |
| } // namespace |
| |
| // A bubble that contains actions available for the selected text. An object of |
| // this type, as a BubbleDialogDelegateView, manages its own lifetime. |
| class TouchSelectionMenuRunnerViews::Menu : public BubbleDialogDelegateView, |
| public ButtonListener { |
| public: |
| Menu(TouchSelectionMenuRunnerViews* owner, |
| ui::TouchSelectionMenuClient* client, |
| const gfx::Rect& anchor_rect, |
| const gfx::Size& handle_image_size, |
| aura::Window* context); |
| |
| // Checks whether there is any command available to show in the menu. |
| static bool IsMenuAvailable(const ui::TouchSelectionMenuClient* client); |
| |
| // Closes the menu. This will eventually self-destroy the object. |
| void CloseMenu(); |
| |
| private: |
| friend class TouchSelectionMenuRunnerViews::TestApi; |
| |
| ~Menu() override; |
| |
| // Queries the |client_| for what commands to show in the menu and sizes the |
| // menu appropriately. |
| void CreateButtons(); |
| |
| // Helper method to create a single button. |
| Button* CreateButton(const base::string16& title, int tag); |
| |
| // Helper to disconnect this menu object from its owning menu runner. |
| void DisconnectOwner(); |
| |
| // BubbleDialogDelegateView: |
| void OnPaint(gfx::Canvas* canvas) override; |
| void WindowClosing() override; |
| int GetDialogButtons() const override; |
| |
| // ButtonListener: |
| void ButtonPressed(Button* sender, const ui::Event& event) override; |
| |
| TouchSelectionMenuRunnerViews* owner_; |
| ui::TouchSelectionMenuClient* const client_; |
| |
| DISALLOW_COPY_AND_ASSIGN(Menu); |
| }; |
| |
| TouchSelectionMenuRunnerViews::Menu::Menu(TouchSelectionMenuRunnerViews* owner, |
| ui::TouchSelectionMenuClient* client, |
| const gfx::Rect& anchor_rect, |
| const gfx::Size& handle_image_size, |
| aura::Window* context) |
| : BubbleDialogDelegateView(nullptr, BubbleBorder::BOTTOM_CENTER), |
| owner_(owner), |
| client_(client) { |
| DCHECK(owner_); |
| DCHECK(client_); |
| |
| set_shadow(BubbleBorder::SMALL_SHADOW); |
| set_parent_window(context); |
| set_margins(gfx::Insets(kMenuMargin, kMenuMargin, kMenuMargin, kMenuMargin)); |
| set_can_activate(false); |
| set_adjust_if_offscreen(true); |
| EnableCanvasFlippingForRTLUI(true); |
| |
| SetLayoutManager(new BoxLayout(BoxLayout::kHorizontal, gfx::Insets(), |
| kSpacingBetweenButtons)); |
| CreateButtons(); |
| |
| // After buttons are created, check if there is enough room between handles to |
| // show the menu and adjust anchor rect properly if needed, just in case the |
| // menu is needed to be shown under the selection. |
| gfx::Rect adjusted_anchor_rect(anchor_rect); |
| int menu_width = GetPreferredSize().width(); |
| // TODO(mfomitchev): This assumes that the handles are center-aligned to the |
| // |achor_rect| edges, which is not true. We should fix this, perhaps by |
| // passing down the cumulative width occupied by the handles within |
| // |anchor_rect| plus the handle image height instead of |handle_image_size|. |
| // Perhaps we should also allow for some minimum padding. |
| if (menu_width > anchor_rect.width() - handle_image_size.width()) |
| adjusted_anchor_rect.Inset(0, 0, 0, -handle_image_size.height()); |
| SetAnchorRect(adjusted_anchor_rect); |
| |
| BubbleDialogDelegateView::CreateBubble(this); |
| GetWidget()->Show(); |
| } |
| |
| bool TouchSelectionMenuRunnerViews::Menu::IsMenuAvailable( |
| const ui::TouchSelectionMenuClient* client) { |
| DCHECK(client); |
| |
| for (size_t i = 0; i < arraysize(kMenuCommands); i++) { |
| if (client->IsCommandIdEnabled(kMenuCommands[i])) |
| return true; |
| } |
| return false; |
| } |
| |
| TouchSelectionMenuRunnerViews::Menu::~Menu() { |
| } |
| |
| void TouchSelectionMenuRunnerViews::Menu::CreateButtons() { |
| for (size_t i = 0; i < arraysize(kMenuCommands); i++) { |
| int command_id = kMenuCommands[i]; |
| if (!client_->IsCommandIdEnabled(command_id)) |
| continue; |
| |
| Button* button = |
| CreateButton(l10n_util::GetStringUTF16(command_id), command_id); |
| AddChildView(button); |
| } |
| |
| // Finally, add ellipses button. |
| AddChildView( |
| CreateButton(base::UTF8ToUTF16(kEllipsesButtonText), kEllipsesButtonTag)); |
| Layout(); |
| } |
| |
| Button* TouchSelectionMenuRunnerViews::Menu::CreateButton( |
| const base::string16& title, |
| int tag) { |
| base::string16 label = |
| gfx::RemoveAcceleratorChar(title, '&', nullptr, nullptr); |
| LabelButton* button = new LabelButton(this, label, style::CONTEXT_TOUCH_MENU); |
| button->SetMinSize(gfx::Size(kMenuButtonMinWidth, kMenuButtonMinHeight)); |
| button->SetFocusForPlatform(); |
| button->SetHorizontalAlignment(gfx::ALIGN_CENTER); |
| button->set_tag(tag); |
| return button; |
| } |
| |
| void TouchSelectionMenuRunnerViews::Menu::CloseMenu() { |
| DisconnectOwner(); |
| // Closing the widget will self-destroy this object. |
| Widget* widget = GetWidget(); |
| if (widget && !widget->IsClosed()) |
| widget->Close(); |
| } |
| |
| void TouchSelectionMenuRunnerViews::Menu::DisconnectOwner() { |
| DCHECK(owner_); |
| owner_->menu_ = nullptr; |
| owner_ = nullptr; |
| } |
| |
| void TouchSelectionMenuRunnerViews::Menu::OnPaint(gfx::Canvas* canvas) { |
| BubbleDialogDelegateView::OnPaint(canvas); |
| |
| // Draw separator bars. |
| for (int i = 0; i < child_count() - 1; ++i) { |
| View* child = child_at(i); |
| int x = child->bounds().right() + kSpacingBetweenButtons / 2; |
| canvas->FillRect(gfx::Rect(x, 0, 1, child->height()), |
| kButtonSeparatorColor); |
| } |
| } |
| |
| void TouchSelectionMenuRunnerViews::Menu::WindowClosing() { |
| DCHECK(!owner_ || owner_->menu_ == this); |
| BubbleDialogDelegateView::WindowClosing(); |
| if (owner_) |
| DisconnectOwner(); |
| } |
| |
| int TouchSelectionMenuRunnerViews::Menu::GetDialogButtons() const { |
| return ui::DIALOG_BUTTON_NONE; |
| } |
| |
| void TouchSelectionMenuRunnerViews::Menu::ButtonPressed( |
| Button* sender, |
| const ui::Event& event) { |
| CloseMenu(); |
| if (sender->tag() != kEllipsesButtonTag) |
| client_->ExecuteCommand(sender->tag(), event.flags()); |
| else |
| client_->RunContextMenu(); |
| } |
| |
| TouchSelectionMenuRunnerViews::TestApi::TestApi( |
| TouchSelectionMenuRunnerViews* menu_runner) |
| : menu_runner_(menu_runner) { |
| DCHECK(menu_runner_); |
| } |
| |
| TouchSelectionMenuRunnerViews::TestApi::~TestApi() {} |
| |
| gfx::Rect TouchSelectionMenuRunnerViews::TestApi::GetAnchorRect() const { |
| TouchSelectionMenuRunnerViews::Menu* menu = menu_runner_->menu_; |
| return menu ? menu->GetAnchorRect() : gfx::Rect(); |
| } |
| |
| Button* TouchSelectionMenuRunnerViews::TestApi::GetFirstButton() const { |
| TouchSelectionMenuRunnerViews::Menu* menu = menu_runner_->menu_; |
| return menu ? static_cast<Button*>(menu->child_at(0)) : nullptr; |
| } |
| |
| Widget* TouchSelectionMenuRunnerViews::TestApi::GetWidget() const { |
| TouchSelectionMenuRunnerViews::Menu* menu = menu_runner_->menu_; |
| return menu ? menu->GetWidget() : nullptr; |
| } |
| TouchSelectionMenuRunnerViews::TouchSelectionMenuRunnerViews() |
| : menu_(nullptr) { |
| } |
| |
| TouchSelectionMenuRunnerViews::~TouchSelectionMenuRunnerViews() { |
| CloseMenu(); |
| } |
| |
| bool TouchSelectionMenuRunnerViews::IsMenuAvailable( |
| const ui::TouchSelectionMenuClient* client) const { |
| return TouchSelectionMenuRunnerViews::Menu::IsMenuAvailable(client); |
| } |
| |
| void TouchSelectionMenuRunnerViews::OpenMenu( |
| ui::TouchSelectionMenuClient* client, |
| const gfx::Rect& anchor_rect, |
| const gfx::Size& handle_image_size, |
| aura::Window* context) { |
| CloseMenu(); |
| |
| if (TouchSelectionMenuRunnerViews::Menu::IsMenuAvailable(client)) |
| menu_ = new Menu(this, client, anchor_rect, handle_image_size, context); |
| } |
| |
| void TouchSelectionMenuRunnerViews::CloseMenu() { |
| if (!menu_) |
| return; |
| |
| // Closing the menu sets |menu_| to nullptr and eventually deletes the object. |
| menu_->CloseMenu(); |
| DCHECK(!menu_); |
| } |
| |
| bool TouchSelectionMenuRunnerViews::IsRunning() const { |
| return menu_ != nullptr; |
| } |
| |
| } // namespace views |