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