Home | History | Annotate | Download | only in button
      1 // Copyright (c) 2012 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/controls/button/menu_button.h"
      6 
      7 #include "base/strings/utf_string_conversions.h"
      8 #include "grit/ui_resources.h"
      9 #include "grit/ui_strings.h"
     10 #include "ui/base/accessibility/accessible_view_state.h"
     11 #include "ui/base/dragdrop/drag_drop_types.h"
     12 #include "ui/base/l10n/l10n_util.h"
     13 #include "ui/base/resource/resource_bundle.h"
     14 #include "ui/events/event.h"
     15 #include "ui/events/event_constants.h"
     16 #include "ui/gfx/canvas.h"
     17 #include "ui/gfx/image/image.h"
     18 #include "ui/gfx/screen.h"
     19 #include "ui/views/controls/button/button.h"
     20 #include "ui/views/controls/button/menu_button_listener.h"
     21 #include "ui/views/mouse_constants.h"
     22 #include "ui/views/widget/root_view.h"
     23 #include "ui/views/widget/widget.h"
     24 
     25 using base::TimeTicks;
     26 using base::TimeDelta;
     27 
     28 namespace views {
     29 
     30 // Default menu offset.
     31 static const int kDefaultMenuOffsetX = -2;
     32 static const int kDefaultMenuOffsetY = -4;
     33 
     34 // static
     35 const char MenuButton::kViewClassName[] = "MenuButton";
     36 const int MenuButton::kMenuMarkerPaddingLeft = 3;
     37 const int MenuButton::kMenuMarkerPaddingRight = -1;
     38 
     39 ////////////////////////////////////////////////////////////////////////////////
     40 //
     41 // MenuButton - constructors, destructors, initialization
     42 //
     43 ////////////////////////////////////////////////////////////////////////////////
     44 
     45 MenuButton::MenuButton(ButtonListener* listener,
     46                        const string16& text,
     47                        MenuButtonListener* menu_button_listener,
     48                        bool show_menu_marker)
     49     : TextButton(listener, text),
     50       menu_visible_(false),
     51       menu_offset_(kDefaultMenuOffsetX, kDefaultMenuOffsetY),
     52       listener_(menu_button_listener),
     53       show_menu_marker_(show_menu_marker),
     54       menu_marker_(ui::ResourceBundle::GetSharedInstance().GetImageNamed(
     55           IDR_MENU_DROPARROW).ToImageSkia()),
     56       destroyed_flag_(NULL) {
     57   set_alignment(TextButton::ALIGN_LEFT);
     58 }
     59 
     60 MenuButton::~MenuButton() {
     61   if (destroyed_flag_)
     62     *destroyed_flag_ = true;
     63 }
     64 
     65 ////////////////////////////////////////////////////////////////////////////////
     66 //
     67 // MenuButton - Public APIs
     68 //
     69 ////////////////////////////////////////////////////////////////////////////////
     70 
     71 bool MenuButton::Activate() {
     72   SetState(STATE_PRESSED);
     73   if (listener_) {
     74     gfx::Rect lb = GetLocalBounds();
     75 
     76     // The position of the menu depends on whether or not the locale is
     77     // right-to-left.
     78     gfx::Point menu_position(lb.right(), lb.bottom());
     79     if (base::i18n::IsRTL())
     80       menu_position.set_x(lb.x());
     81 
     82     View::ConvertPointToScreen(this, &menu_position);
     83     if (base::i18n::IsRTL())
     84       menu_position.Offset(-menu_offset_.x(), menu_offset_.y());
     85     else
     86       menu_position.Offset(menu_offset_.x(), menu_offset_.y());
     87 
     88     int max_x_coordinate = GetMaximumScreenXCoordinate();
     89     if (max_x_coordinate && max_x_coordinate <= menu_position.x())
     90       menu_position.set_x(max_x_coordinate - 1);
     91 
     92     // We're about to show the menu from a mouse press. By showing from the
     93     // mouse press event we block RootView in mouse dispatching. This also
     94     // appears to cause RootView to get a mouse pressed BEFORE the mouse
     95     // release is seen, which means RootView sends us another mouse press no
     96     // matter where the user pressed. To force RootView to recalculate the
     97     // mouse target during the mouse press we explicitly set the mouse handler
     98     // to NULL.
     99     static_cast<internal::RootView*>(GetWidget()->GetRootView())->
    100         SetMouseHandler(NULL);
    101 
    102     menu_visible_ = true;
    103 
    104     bool destroyed = false;
    105     destroyed_flag_ = &destroyed;
    106 
    107     listener_->OnMenuButtonClicked(this, menu_position);
    108 
    109     if (destroyed) {
    110       // The menu was deleted while showing. Don't attempt any processing.
    111       return false;
    112     }
    113 
    114     destroyed_flag_ = NULL;
    115 
    116     menu_visible_ = false;
    117     menu_closed_time_ = TimeTicks::Now();
    118 
    119     // Now that the menu has closed, we need to manually reset state to
    120     // "normal" since the menu modal loop will have prevented normal
    121     // mouse move messages from getting to this View. We set "normal"
    122     // and not "hot" because the likelihood is that the mouse is now
    123     // somewhere else (user clicked elsewhere on screen to close the menu
    124     // or selected an item) and we will inevitably refresh the hot state
    125     // in the event the mouse _is_ over the view.
    126     SetState(STATE_NORMAL);
    127 
    128     // We must return false here so that the RootView does not get stuck
    129     // sending all mouse pressed events to us instead of the appropriate
    130     // target.
    131     return false;
    132   }
    133   return true;
    134 }
    135 
    136 void MenuButton::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) {
    137   TextButton::PaintButton(canvas, mode);
    138 
    139   if (show_menu_marker_)
    140     PaintMenuMarker(canvas);
    141 }
    142 
    143 ////////////////////////////////////////////////////////////////////////////////
    144 //
    145 // MenuButton - Events
    146 //
    147 ////////////////////////////////////////////////////////////////////////////////
    148 
    149 gfx::Size MenuButton::GetPreferredSize() {
    150   gfx::Size prefsize = TextButton::GetPreferredSize();
    151   if (show_menu_marker_) {
    152     prefsize.Enlarge(menu_marker_->width() + kMenuMarkerPaddingLeft +
    153                          kMenuMarkerPaddingRight,
    154                      0);
    155   }
    156   return prefsize;
    157 }
    158 
    159 const char* MenuButton::GetClassName() const {
    160   return kViewClassName;
    161 }
    162 
    163 bool MenuButton::OnMousePressed(const ui::MouseEvent& event) {
    164   RequestFocus();
    165   if (state() != STATE_DISABLED) {
    166     // If we're draggable (GetDragOperations returns a non-zero value), then
    167     // don't pop on press, instead wait for release.
    168     if (event.IsOnlyLeftMouseButton() &&
    169         HitTestPoint(event.location()) &&
    170         GetDragOperations(event.location()) == ui::DragDropTypes::DRAG_NONE) {
    171       TimeDelta delta = TimeTicks::Now() - menu_closed_time_;
    172       if (delta.InMilliseconds() > kMinimumMsBetweenButtonClicks)
    173         return Activate();
    174     }
    175   }
    176   return true;
    177 }
    178 
    179 void MenuButton::OnMouseReleased(const ui::MouseEvent& event) {
    180   // Explicitly test for left mouse button to show the menu. If we tested for
    181   // !IsTriggerableEvent it could lead to a situation where we end up showing
    182   // the menu and context menu (this would happen if the right button is not
    183   // triggerable and there's a context menu).
    184   if (GetDragOperations(event.location()) != ui::DragDropTypes::DRAG_NONE &&
    185       state() != STATE_DISABLED && !InDrag() && event.IsOnlyLeftMouseButton() &&
    186       HitTestPoint(event.location())) {
    187     Activate();
    188   } else {
    189     TextButton::OnMouseReleased(event);
    190   }
    191 }
    192 
    193 // The reason we override View::OnMouseExited is because we get this event when
    194 // we display the menu. If we don't override this method then
    195 // BaseButton::OnMouseExited will get the event and will set the button's state
    196 // to STATE_NORMAL instead of keeping the state BM_PUSHED. This, in turn, will
    197 // cause the button to appear depressed while the menu is displayed.
    198 void MenuButton::OnMouseExited(const ui::MouseEvent& event) {
    199   if ((state_ != STATE_DISABLED) && (!menu_visible_) && (!InDrag())) {
    200     SetState(STATE_NORMAL);
    201   }
    202 }
    203 
    204 void MenuButton::OnGestureEvent(ui::GestureEvent* event) {
    205   if (state() != STATE_DISABLED && event->type() == ui::ET_GESTURE_TAP) {
    206     if (Activate())
    207       event->StopPropagation();
    208     return;
    209   }
    210   TextButton::OnGestureEvent(event);
    211 }
    212 
    213 bool MenuButton::OnKeyPressed(const ui::KeyEvent& event) {
    214   switch (event.key_code()) {
    215     case ui::VKEY_SPACE:
    216       // Alt-space on windows should show the window menu.
    217       if (event.IsAltDown())
    218         break;
    219     case ui::VKEY_RETURN:
    220     case ui::VKEY_UP:
    221     case ui::VKEY_DOWN: {
    222       // WARNING: we may have been deleted by the time Activate returns.
    223       bool ret = Activate();
    224 #if defined(USE_AURA)
    225       // This is to prevent the keyboard event from being dispatched twice.
    226       // The Activate function returns false in most cases. In AURA if the
    227       // keyboard event is not handled, we pass it to the default handler
    228       // which dispatches the event back to us causing the menu to get
    229       // displayed again.
    230       ret = true;
    231 #endif
    232       return ret;
    233     }
    234     default:
    235       break;
    236   }
    237   return false;
    238 }
    239 
    240 bool MenuButton::OnKeyReleased(const ui::KeyEvent& event) {
    241   // Override CustomButton's implementation, which presses the button when
    242   // you press space and clicks it when you release space.  For a MenuButton
    243   // we always activate the menu on key press.
    244   return false;
    245 }
    246 
    247 void MenuButton::GetAccessibleState(ui::AccessibleViewState* state) {
    248   CustomButton::GetAccessibleState(state);
    249   state->role = ui::AccessibilityTypes::ROLE_BUTTONMENU;
    250   state->default_action = l10n_util::GetStringUTF16(IDS_APP_ACCACTION_PRESS);
    251   state->state = ui::AccessibilityTypes::STATE_HASPOPUP;
    252 }
    253 
    254 void MenuButton::PaintMenuMarker(gfx::Canvas* canvas) {
    255   gfx::Insets insets = GetInsets();
    256 
    257   // We can not use the views' mirroring infrastructure for mirroring a
    258   // MenuButton control (see TextButton::OnPaint() for a detailed explanation
    259   // regarding why we can not flip the canvas). Therefore, we need to
    260   // manually mirror the position of the down arrow.
    261   gfx::Rect arrow_bounds(width() - insets.right() -
    262                          menu_marker_->width() - kMenuMarkerPaddingRight,
    263                          height() / 2 - menu_marker_->height() / 2,
    264                          menu_marker_->width(),
    265                          menu_marker_->height());
    266   arrow_bounds.set_x(GetMirroredXForRect(arrow_bounds));
    267   canvas->DrawImageInt(*menu_marker_, arrow_bounds.x(), arrow_bounds.y());
    268 }
    269 
    270 int MenuButton::GetMaximumScreenXCoordinate() {
    271   if (!GetWidget()) {
    272     NOTREACHED();
    273     return 0;
    274   }
    275 
    276   gfx::Rect monitor_bounds = GetWidget()->GetWorkAreaBoundsInScreen();
    277   return monitor_bounds.right() - 1;
    278 }
    279 
    280 }  // namespace views
    281