| // Copyright (c) 2011 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 "views/controls/button/menu_button.h" |
| |
| #include "base/utf_string_conversions.h" |
| #include "grit/ui_resources.h" |
| #include "grit/ui_strings.h" |
| #include "ui/base/accessibility/accessible_view_state.h" |
| #include "ui/base/dragdrop/drag_drop_types.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/screen.h" |
| #include "views/controls/button/button.h" |
| #include "views/controls/menu/view_menu_delegate.h" |
| #include "views/events/event.h" |
| #include "views/widget/root_view.h" |
| #include "views/widget/widget.h" |
| |
| using base::Time; |
| using base::TimeDelta; |
| |
| namespace views { |
| |
| // The amount of time, in milliseconds, we wait before allowing another mouse |
| // pressed event to show the menu. |
| static const int64 kMinimumTimeBetweenButtonClicks = 100; |
| |
| // How much padding to put on the left and right of the menu marker. |
| static const int kMenuMarkerPaddingLeft = 3; |
| static const int kMenuMarkerPaddingRight = -1; |
| |
| // Default menu offset. |
| static const int kDefaultMenuOffsetX = -2; |
| static const int kDefaultMenuOffsetY = -4; |
| |
| // static |
| const char MenuButton::kViewClassName[] = "views/MenuButton"; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // |
| // MenuButton - constructors, destructors, initialization |
| // |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| MenuButton::MenuButton(ButtonListener* listener, |
| const string16& text, |
| ViewMenuDelegate* menu_delegate, |
| bool show_menu_marker) |
| : TextButton(listener, text), |
| menu_visible_(false), |
| menu_offset_(kDefaultMenuOffsetX, kDefaultMenuOffsetY), |
| menu_delegate_(menu_delegate), |
| show_menu_marker_(show_menu_marker), |
| menu_marker_(ResourceBundle::GetSharedInstance().GetBitmapNamed( |
| IDR_MENU_DROPARROW)), |
| destroyed_flag_(NULL) { |
| set_alignment(TextButton::ALIGN_LEFT); |
| } |
| |
| MenuButton::~MenuButton() { |
| if (destroyed_flag_) |
| *destroyed_flag_ = true; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // |
| // MenuButton - Public APIs |
| // |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| bool MenuButton::Activate() { |
| SetState(BS_PUSHED); |
| if (menu_delegate_) { |
| gfx::Rect lb = GetLocalBounds(); |
| |
| // The position of the menu depends on whether or not the locale is |
| // right-to-left. |
| gfx::Point menu_position(lb.right(), lb.bottom()); |
| if (base::i18n::IsRTL()) |
| menu_position.set_x(lb.x()); |
| |
| View::ConvertPointToScreen(this, &menu_position); |
| if (base::i18n::IsRTL()) |
| menu_position.Offset(-menu_offset_.x(), menu_offset_.y()); |
| else |
| menu_position.Offset(menu_offset_.x(), menu_offset_.y()); |
| |
| int max_x_coordinate = GetMaximumScreenXCoordinate(); |
| if (max_x_coordinate && max_x_coordinate <= menu_position.x()) |
| menu_position.set_x(max_x_coordinate - 1); |
| |
| // We're about to show the menu from a mouse press. By showing from the |
| // mouse press event we block RootView in mouse dispatching. This also |
| // appears to cause RootView to get a mouse pressed BEFORE the mouse |
| // release is seen, which means RootView sends us another mouse press no |
| // matter where the user pressed. To force RootView to recalculate the |
| // mouse target during the mouse press we explicitly set the mouse handler |
| // to NULL. |
| static_cast<internal::RootView*>(GetWidget()->GetRootView())-> |
| SetMouseHandler(NULL); |
| |
| menu_visible_ = true; |
| |
| bool destroyed = false; |
| destroyed_flag_ = &destroyed; |
| |
| menu_delegate_->RunMenu(this, menu_position); |
| |
| if (destroyed) { |
| // The menu was deleted while showing. Don't attempt any processing. |
| return false; |
| } |
| |
| destroyed_flag_ = NULL; |
| |
| menu_visible_ = false; |
| menu_closed_time_ = Time::Now(); |
| |
| // Now that the menu has closed, we need to manually reset state to |
| // "normal" since the menu modal loop will have prevented normal |
| // mouse move messages from getting to this View. We set "normal" |
| // and not "hot" because the likelihood is that the mouse is now |
| // somewhere else (user clicked elsewhere on screen to close the menu |
| // or selected an item) and we will inevitably refresh the hot state |
| // in the event the mouse _is_ over the view. |
| SetState(BS_NORMAL); |
| |
| // We must return false here so that the RootView does not get stuck |
| // sending all mouse pressed events to us instead of the appropriate |
| // target. |
| return false; |
| } |
| return true; |
| } |
| |
| void MenuButton::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) { |
| TextButton::PaintButton(canvas, mode); |
| |
| if (show_menu_marker_) { |
| gfx::Insets insets = GetInsets(); |
| |
| // We can not use the views' mirroring infrastructure for mirroring a |
| // MenuButton control (see TextButton::OnPaint() for a detailed explanation |
| // regarding why we can not flip the canvas). Therefore, we need to |
| // manually mirror the position of the down arrow. |
| gfx::Rect arrow_bounds(width() - insets.right() - |
| menu_marker_->width() - kMenuMarkerPaddingRight, |
| height() / 2 - menu_marker_->height() / 2, |
| menu_marker_->width(), |
| menu_marker_->height()); |
| arrow_bounds.set_x(GetMirroredXForRect(arrow_bounds)); |
| canvas->DrawBitmapInt(*menu_marker_, arrow_bounds.x(), arrow_bounds.y()); |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // |
| // MenuButton - Events |
| // |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| gfx::Size MenuButton::GetPreferredSize() { |
| gfx::Size prefsize = TextButton::GetPreferredSize(); |
| if (show_menu_marker_) { |
| prefsize.Enlarge(menu_marker_->width() + kMenuMarkerPaddingLeft + |
| kMenuMarkerPaddingRight, |
| 0); |
| } |
| return prefsize; |
| } |
| |
| std::string MenuButton::GetClassName() const { |
| return kViewClassName; |
| } |
| |
| bool MenuButton::OnMousePressed(const MouseEvent& event) { |
| RequestFocus(); |
| if (state() != BS_DISABLED) { |
| // If we're draggable (GetDragOperations returns a non-zero value), then |
| // don't pop on press, instead wait for release. |
| if (event.IsOnlyLeftMouseButton() && HitTest(event.location()) && |
| GetDragOperations(event.location()) == ui::DragDropTypes::DRAG_NONE) { |
| TimeDelta delta = Time::Now() - menu_closed_time_; |
| int64 delta_in_milliseconds = delta.InMilliseconds(); |
| if (delta_in_milliseconds > kMinimumTimeBetweenButtonClicks) { |
| return Activate(); |
| } |
| } |
| } |
| return true; |
| } |
| |
| void MenuButton::OnMouseReleased(const MouseEvent& event) { |
| // Explicitly test for left mouse button to show the menu. If we tested for |
| // !IsTriggerableEvent it could lead to a situation where we end up showing |
| // the menu and context menu (this would happen if the right button is not |
| // triggerable and there's a context menu). |
| if (GetDragOperations(event.location()) != ui::DragDropTypes::DRAG_NONE && |
| state() != BS_DISABLED && !InDrag() && event.IsOnlyLeftMouseButton() && |
| HitTest(event.location())) { |
| Activate(); |
| } else { |
| TextButton::OnMouseReleased(event); |
| } |
| } |
| |
| // The reason we override View::OnMouseExited is because we get this event when |
| // we display the menu. If we don't override this method then |
| // BaseButton::OnMouseExited will get the event and will set the button's state |
| // to BS_NORMAL instead of keeping the state BM_PUSHED. This, in turn, will |
| // cause the button to appear depressed while the menu is displayed. |
| void MenuButton::OnMouseExited(const MouseEvent& event) { |
| if ((state_ != BS_DISABLED) && (!menu_visible_) && (!InDrag())) { |
| SetState(BS_NORMAL); |
| } |
| } |
| |
| bool MenuButton::OnKeyPressed(const KeyEvent& event) { |
| switch (event.key_code()) { |
| case ui::VKEY_SPACE: |
| // Alt-space on windows should show the window menu. |
| if (event.IsAltDown()) |
| break; |
| case ui::VKEY_RETURN: |
| case ui::VKEY_UP: |
| case ui::VKEY_DOWN: { |
| // WARNING: we may have been deleted by the time Activate returns. |
| return Activate(); |
| } |
| default: |
| break; |
| } |
| return false; |
| } |
| |
| bool MenuButton::OnKeyReleased(const KeyEvent& event) { |
| // Override CustomButton's implementation, which presses the button when |
| // you press space and clicks it when you release space. For a MenuButton |
| // we always activate the menu on key press. |
| return false; |
| } |
| |
| void MenuButton::GetAccessibleState(ui::AccessibleViewState* state) { |
| CustomButton::GetAccessibleState(state); |
| state->role = ui::AccessibilityTypes::ROLE_BUTTONMENU; |
| state->default_action = l10n_util::GetStringUTF16(IDS_APP_ACCACTION_PRESS); |
| state->state = ui::AccessibilityTypes::STATE_HASPOPUP; |
| } |
| |
| int MenuButton::GetMaximumScreenXCoordinate() { |
| if (!GetWidget()) { |
| NOTREACHED(); |
| return 0; |
| } |
| |
| gfx::Rect monitor_bounds = GetWidget()->GetWorkAreaBoundsInScreen(); |
| return monitor_bounds.right() - 1; |
| } |
| |
| } // namespace views |