Home | History | Annotate | Download | only in menu
      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/menu/submenu_view.h"
      6 
      7 #include <algorithm>
      8 
      9 #include "base/compiler_specific.h"
     10 #include "ui/accessibility/ax_view_state.h"
     11 #include "ui/events/event.h"
     12 #include "ui/gfx/canvas.h"
     13 #include "ui/gfx/geometry/safe_integer_conversions.h"
     14 #include "ui/views/controls/menu/menu_config.h"
     15 #include "ui/views/controls/menu/menu_controller.h"
     16 #include "ui/views/controls/menu/menu_host.h"
     17 #include "ui/views/controls/menu/menu_item_view.h"
     18 #include "ui/views/controls/menu/menu_scroll_view_container.h"
     19 #include "ui/views/widget/root_view.h"
     20 #include "ui/views/widget/widget.h"
     21 
     22 namespace {
     23 
     24 // Height of the drop indicator. This should be an even number.
     25 const int kDropIndicatorHeight = 2;
     26 
     27 // Color of the drop indicator.
     28 const SkColor kDropIndicatorColor = SK_ColorBLACK;
     29 
     30 }  // namespace
     31 
     32 namespace views {
     33 
     34 // static
     35 const char SubmenuView::kViewClassName[] = "SubmenuView";
     36 
     37 SubmenuView::SubmenuView(MenuItemView* parent)
     38     : parent_menu_item_(parent),
     39       host_(NULL),
     40       drop_item_(NULL),
     41       drop_position_(MenuDelegate::DROP_NONE),
     42       scroll_view_container_(NULL),
     43       max_minor_text_width_(0),
     44       minimum_preferred_width_(0),
     45       resize_open_menu_(false),
     46       scroll_animator_(new ScrollAnimator(this)),
     47       roundoff_error_(0),
     48       prefix_selector_(this) {
     49   DCHECK(parent);
     50   // We'll delete ourselves, otherwise the ScrollView would delete us on close.
     51   set_owned_by_client();
     52 }
     53 
     54 SubmenuView::~SubmenuView() {
     55   // The menu may not have been closed yet (it will be hidden, but not
     56   // necessarily closed).
     57   Close();
     58 
     59   delete scroll_view_container_;
     60 }
     61 
     62 int SubmenuView::GetMenuItemCount() {
     63   int count = 0;
     64   for (int i = 0; i < child_count(); ++i) {
     65     if (child_at(i)->id() == MenuItemView::kMenuItemViewID)
     66       count++;
     67   }
     68   return count;
     69 }
     70 
     71 MenuItemView* SubmenuView::GetMenuItemAt(int index) {
     72   for (int i = 0, count = 0; i < child_count(); ++i) {
     73     if (child_at(i)->id() == MenuItemView::kMenuItemViewID &&
     74         count++ == index) {
     75       return static_cast<MenuItemView*>(child_at(i));
     76     }
     77   }
     78   NOTREACHED();
     79   return NULL;
     80 }
     81 
     82 void SubmenuView::ChildPreferredSizeChanged(View* child) {
     83   if (!resize_open_menu_)
     84     return;
     85 
     86   MenuItemView *item = GetMenuItem();
     87   MenuController* controller = item->GetMenuController();
     88 
     89   if (controller) {
     90     bool dir;
     91     gfx::Rect bounds = controller->CalculateMenuBounds(item, false, &dir);
     92     Reposition(bounds);
     93   }
     94 }
     95 
     96 void SubmenuView::Layout() {
     97   // We're in a ScrollView, and need to set our width/height ourselves.
     98   if (!parent())
     99     return;
    100 
    101   // Use our current y, unless it means part of the menu isn't visible anymore.
    102   int pref_height = GetPreferredSize().height();
    103   int new_y;
    104   if (pref_height > parent()->height())
    105     new_y = std::max(parent()->height() - pref_height, y());
    106   else
    107     new_y = 0;
    108   SetBounds(x(), new_y, parent()->width(), pref_height);
    109 
    110   gfx::Insets insets = GetInsets();
    111   int x = insets.left();
    112   int y = insets.top();
    113   int menu_item_width = width() - insets.width();
    114   for (int i = 0; i < child_count(); ++i) {
    115     View* child = child_at(i);
    116     if (child->visible()) {
    117       int child_height = child->GetHeightForWidth(menu_item_width);
    118       child->SetBounds(x, y, menu_item_width, child_height);
    119       y += child_height;
    120     }
    121   }
    122 }
    123 
    124 gfx::Size SubmenuView::GetPreferredSize() const {
    125   if (!has_children())
    126     return gfx::Size();
    127 
    128   max_minor_text_width_ = 0;
    129   // The maximum width of items which contain maybe a label and multiple views.
    130   int max_complex_width = 0;
    131   // The max. width of items which contain a label and maybe an accelerator.
    132   int max_simple_width = 0;
    133 
    134   // We perform the size calculation in two passes. In the first pass, we
    135   // calculate the width of the menu. In the second, we calculate the height
    136   // using that width. This allows views that have flexible widths to adjust
    137   // accordingly.
    138   for (int i = 0; i < child_count(); ++i) {
    139     const View* child = child_at(i);
    140     if (!child->visible())
    141       continue;
    142     if (child->id() == MenuItemView::kMenuItemViewID) {
    143       const MenuItemView* menu = static_cast<const MenuItemView*>(child);
    144       const MenuItemView::MenuItemDimensions& dimensions =
    145           menu->GetDimensions();
    146       max_simple_width = std::max(
    147           max_simple_width, dimensions.standard_width);
    148       max_minor_text_width_ =
    149           std::max(max_minor_text_width_, dimensions.minor_text_width);
    150       max_complex_width = std::max(max_complex_width,
    151           dimensions.standard_width + dimensions.children_width);
    152     } else {
    153       max_complex_width = std::max(max_complex_width,
    154                                    child->GetPreferredSize().width());
    155     }
    156   }
    157   if (max_minor_text_width_ > 0) {
    158     max_minor_text_width_ +=
    159         GetMenuItem()->GetMenuConfig().label_to_minor_text_padding;
    160   }
    161   // Finish calculating our optimum width.
    162   gfx::Insets insets = GetInsets();
    163   int width = std::max(max_complex_width,
    164                        std::max(max_simple_width + max_minor_text_width_ +
    165                                     insets.width(),
    166                                 minimum_preferred_width_ - 2 * insets.width()));
    167 
    168   // Then, the height for that width.
    169   int height = 0;
    170   int menu_item_width = width - insets.width();
    171   for (int i = 0; i < child_count(); ++i) {
    172     const View* child = child_at(i);
    173     height += child->visible() ? child->GetHeightForWidth(menu_item_width) : 0;
    174   }
    175 
    176   return gfx::Size(width, height + insets.height());
    177 }
    178 
    179 void SubmenuView::GetAccessibleState(ui::AXViewState* state) {
    180   // Inherit most of the state from the parent menu item, except the role.
    181   if (GetMenuItem())
    182     GetMenuItem()->GetAccessibleState(state);
    183   state->role = ui::AX_ROLE_MENU_LIST_POPUP;
    184 }
    185 
    186 ui::TextInputClient* SubmenuView::GetTextInputClient() {
    187   return &prefix_selector_;
    188 }
    189 
    190 void SubmenuView::PaintChildren(gfx::Canvas* canvas,
    191                                 const views::CullSet& cull_set) {
    192   View::PaintChildren(canvas, cull_set);
    193 
    194   if (drop_item_ && drop_position_ != MenuDelegate::DROP_ON)
    195     PaintDropIndicator(canvas, drop_item_, drop_position_);
    196 }
    197 
    198 bool SubmenuView::GetDropFormats(
    199       int* formats,
    200       std::set<OSExchangeData::CustomFormat>* custom_formats) {
    201   DCHECK(GetMenuItem()->GetMenuController());
    202   return GetMenuItem()->GetMenuController()->GetDropFormats(this, formats,
    203                                                             custom_formats);
    204 }
    205 
    206 bool SubmenuView::AreDropTypesRequired() {
    207   DCHECK(GetMenuItem()->GetMenuController());
    208   return GetMenuItem()->GetMenuController()->AreDropTypesRequired(this);
    209 }
    210 
    211 bool SubmenuView::CanDrop(const OSExchangeData& data) {
    212   DCHECK(GetMenuItem()->GetMenuController());
    213   return GetMenuItem()->GetMenuController()->CanDrop(this, data);
    214 }
    215 
    216 void SubmenuView::OnDragEntered(const ui::DropTargetEvent& event) {
    217   DCHECK(GetMenuItem()->GetMenuController());
    218   GetMenuItem()->GetMenuController()->OnDragEntered(this, event);
    219 }
    220 
    221 int SubmenuView::OnDragUpdated(const ui::DropTargetEvent& event) {
    222   DCHECK(GetMenuItem()->GetMenuController());
    223   return GetMenuItem()->GetMenuController()->OnDragUpdated(this, event);
    224 }
    225 
    226 void SubmenuView::OnDragExited() {
    227   DCHECK(GetMenuItem()->GetMenuController());
    228   GetMenuItem()->GetMenuController()->OnDragExited(this);
    229 }
    230 
    231 int SubmenuView::OnPerformDrop(const ui::DropTargetEvent& event) {
    232   DCHECK(GetMenuItem()->GetMenuController());
    233   return GetMenuItem()->GetMenuController()->OnPerformDrop(this, event);
    234 }
    235 
    236 bool SubmenuView::OnMouseWheel(const ui::MouseWheelEvent& e) {
    237   gfx::Rect vis_bounds = GetVisibleBounds();
    238   int menu_item_count = GetMenuItemCount();
    239   if (vis_bounds.height() == height() || !menu_item_count) {
    240     // All menu items are visible, nothing to scroll.
    241     return true;
    242   }
    243 
    244   // Find the index of the first menu item whose y-coordinate is >= visible
    245   // y-coordinate.
    246   int i = 0;
    247   while ((i < menu_item_count) && (GetMenuItemAt(i)->y() < vis_bounds.y()))
    248     ++i;
    249   if (i == menu_item_count)
    250     return true;
    251   int first_vis_index = std::max(0,
    252       (GetMenuItemAt(i)->y() == vis_bounds.y()) ? i : i - 1);
    253 
    254   // If the first item isn't entirely visible, make it visible, otherwise make
    255   // the next/previous one entirely visible. If enough wasn't scrolled to show
    256   // any new rows, then just scroll the amount so that smooth scrolling using
    257   // the trackpad is possible.
    258   int delta = abs(e.y_offset() / ui::MouseWheelEvent::kWheelDelta);
    259   if (delta == 0)
    260     return OnScroll(0, e.y_offset());
    261   for (bool scroll_up = (e.y_offset() > 0); delta != 0; --delta) {
    262     int scroll_target;
    263     if (scroll_up) {
    264       if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y()) {
    265         if (first_vis_index == 0)
    266           break;
    267         first_vis_index--;
    268       }
    269       scroll_target = GetMenuItemAt(first_vis_index)->y();
    270     } else {
    271       if (first_vis_index + 1 == menu_item_count)
    272         break;
    273       scroll_target = GetMenuItemAt(first_vis_index + 1)->y();
    274       if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y())
    275         first_vis_index++;
    276     }
    277     ScrollRectToVisible(gfx::Rect(gfx::Point(0, scroll_target),
    278                                   vis_bounds.size()));
    279     vis_bounds = GetVisibleBounds();
    280   }
    281 
    282   return true;
    283 }
    284 
    285 void SubmenuView::OnGestureEvent(ui::GestureEvent* event) {
    286   bool handled = true;
    287   switch (event->type()) {
    288     case ui::ET_GESTURE_SCROLL_BEGIN:
    289       scroll_animator_->Stop();
    290       break;
    291     case ui::ET_GESTURE_SCROLL_UPDATE:
    292       handled = OnScroll(0, event->details().scroll_y());
    293       break;
    294     case ui::ET_GESTURE_SCROLL_END:
    295       break;
    296     case ui::ET_SCROLL_FLING_START:
    297       if (event->details().velocity_y() != 0.0f)
    298         scroll_animator_->Start(0, event->details().velocity_y());
    299       break;
    300     case ui::ET_GESTURE_TAP_DOWN:
    301     case ui::ET_SCROLL_FLING_CANCEL:
    302       if (scroll_animator_->is_scrolling())
    303         scroll_animator_->Stop();
    304       else
    305         handled = false;
    306       break;
    307     default:
    308       handled = false;
    309       break;
    310   }
    311   if (handled)
    312     event->SetHandled();
    313 }
    314 
    315 int SubmenuView::GetRowCount() {
    316   return GetMenuItemCount();
    317 }
    318 
    319 int SubmenuView::GetSelectedRow() {
    320   int row = 0;
    321   for (int i = 0; i < child_count(); ++i) {
    322     if (child_at(i)->id() != MenuItemView::kMenuItemViewID)
    323       continue;
    324 
    325     if (static_cast<MenuItemView*>(child_at(i))->IsSelected())
    326       return row;
    327 
    328     row++;
    329   }
    330 
    331   return -1;
    332 }
    333 
    334 void SubmenuView::SetSelectedRow(int row) {
    335   GetMenuItem()->GetMenuController()->SetSelection(
    336       GetMenuItemAt(row),
    337       MenuController::SELECTION_DEFAULT);
    338 }
    339 
    340 base::string16 SubmenuView::GetTextForRow(int row) {
    341   return GetMenuItemAt(row)->title();
    342 }
    343 
    344 bool SubmenuView::IsShowing() {
    345   return host_ && host_->IsMenuHostVisible();
    346 }
    347 
    348 void SubmenuView::ShowAt(Widget* parent,
    349                          const gfx::Rect& bounds,
    350                          bool do_capture) {
    351   if (host_) {
    352     host_->ShowMenuHost(do_capture);
    353   } else {
    354     host_ = new MenuHost(this);
    355     // Force construction of the scroll view container.
    356     GetScrollViewContainer();
    357     // Force a layout since our preferred size may not have changed but our
    358     // content may have.
    359     InvalidateLayout();
    360     host_->InitMenuHost(parent, bounds, scroll_view_container_, do_capture);
    361   }
    362 
    363   GetScrollViewContainer()->NotifyAccessibilityEvent(
    364       ui::AX_EVENT_MENU_START,
    365       true);
    366   NotifyAccessibilityEvent(
    367       ui::AX_EVENT_MENU_POPUP_START,
    368       true);
    369 }
    370 
    371 void SubmenuView::Reposition(const gfx::Rect& bounds) {
    372   if (host_)
    373     host_->SetMenuHostBounds(bounds);
    374 }
    375 
    376 void SubmenuView::Close() {
    377   if (host_) {
    378     NotifyAccessibilityEvent(ui::AX_EVENT_MENU_POPUP_END, true);
    379     GetScrollViewContainer()->NotifyAccessibilityEvent(
    380         ui::AX_EVENT_MENU_END, true);
    381 
    382     host_->DestroyMenuHost();
    383     host_ = NULL;
    384   }
    385 }
    386 
    387 void SubmenuView::Hide() {
    388   if (host_)
    389     host_->HideMenuHost();
    390   if (scroll_animator_->is_scrolling())
    391     scroll_animator_->Stop();
    392 }
    393 
    394 void SubmenuView::ReleaseCapture() {
    395   if (host_)
    396     host_->ReleaseMenuHostCapture();
    397 }
    398 
    399 bool SubmenuView::SkipDefaultKeyEventProcessing(const ui::KeyEvent& e) {
    400   return views::FocusManager::IsTabTraversalKeyEvent(e);
    401 }
    402 
    403 MenuItemView* SubmenuView::GetMenuItem() const {
    404   return parent_menu_item_;
    405 }
    406 
    407 void SubmenuView::SetDropMenuItem(MenuItemView* item,
    408                                   MenuDelegate::DropPosition position) {
    409   if (drop_item_ == item && drop_position_ == position)
    410     return;
    411   SchedulePaintForDropIndicator(drop_item_, drop_position_);
    412   drop_item_ = item;
    413   drop_position_ = position;
    414   SchedulePaintForDropIndicator(drop_item_, drop_position_);
    415 }
    416 
    417 bool SubmenuView::GetShowSelection(MenuItemView* item) {
    418   if (drop_item_ == NULL)
    419     return true;
    420   // Something is being dropped on one of this menus items. Show the
    421   // selection if the drop is on the passed in item and the drop position is
    422   // ON.
    423   return (drop_item_ == item && drop_position_ == MenuDelegate::DROP_ON);
    424 }
    425 
    426 MenuScrollViewContainer* SubmenuView::GetScrollViewContainer() {
    427   if (!scroll_view_container_) {
    428     scroll_view_container_ = new MenuScrollViewContainer(this);
    429     // Otherwise MenuHost would delete us.
    430     scroll_view_container_->set_owned_by_client();
    431   }
    432   return scroll_view_container_;
    433 }
    434 
    435 void SubmenuView::MenuHostDestroyed() {
    436   host_ = NULL;
    437   GetMenuItem()->GetMenuController()->Cancel(MenuController::EXIT_DESTROYED);
    438 }
    439 
    440 const char* SubmenuView::GetClassName() const {
    441   return kViewClassName;
    442 }
    443 
    444 void SubmenuView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
    445   SchedulePaint();
    446 }
    447 
    448 void SubmenuView::PaintDropIndicator(gfx::Canvas* canvas,
    449                                      MenuItemView* item,
    450                                      MenuDelegate::DropPosition position) {
    451   if (position == MenuDelegate::DROP_NONE)
    452     return;
    453 
    454   gfx::Rect bounds = CalculateDropIndicatorBounds(item, position);
    455   canvas->FillRect(bounds, kDropIndicatorColor);
    456 }
    457 
    458 void SubmenuView::SchedulePaintForDropIndicator(
    459     MenuItemView* item,
    460     MenuDelegate::DropPosition position) {
    461   if (item == NULL)
    462     return;
    463 
    464   if (position == MenuDelegate::DROP_ON) {
    465     item->SchedulePaint();
    466   } else if (position != MenuDelegate::DROP_NONE) {
    467     SchedulePaintInRect(CalculateDropIndicatorBounds(item, position));
    468   }
    469 }
    470 
    471 gfx::Rect SubmenuView::CalculateDropIndicatorBounds(
    472     MenuItemView* item,
    473     MenuDelegate::DropPosition position) {
    474   DCHECK(position != MenuDelegate::DROP_NONE);
    475   gfx::Rect item_bounds = item->bounds();
    476   switch (position) {
    477     case MenuDelegate::DROP_BEFORE:
    478       item_bounds.Offset(0, -kDropIndicatorHeight / 2);
    479       item_bounds.set_height(kDropIndicatorHeight);
    480       return item_bounds;
    481 
    482     case MenuDelegate::DROP_AFTER:
    483       item_bounds.Offset(0, item_bounds.height() - kDropIndicatorHeight / 2);
    484       item_bounds.set_height(kDropIndicatorHeight);
    485       return item_bounds;
    486 
    487     default:
    488       // Don't render anything for on.
    489       return gfx::Rect();
    490   }
    491 }
    492 
    493 bool SubmenuView::OnScroll(float dx, float dy) {
    494   const gfx::Rect& vis_bounds = GetVisibleBounds();
    495   const gfx::Rect& full_bounds = bounds();
    496   int x = vis_bounds.x();
    497   float y_f = vis_bounds.y() - dy - roundoff_error_;
    498   int y = gfx::ToRoundedInt(y_f);
    499   roundoff_error_ = y - y_f;
    500   // clamp y to [0, full_height - vis_height)
    501   y = std::min(y, full_bounds.height() - vis_bounds.height() - 1);
    502   y = std::max(y, 0);
    503   gfx::Rect new_vis_bounds(x, y, vis_bounds.width(), vis_bounds.height());
    504   if (new_vis_bounds != vis_bounds) {
    505     ScrollRectToVisible(new_vis_bounds);
    506     return true;
    507   }
    508   return false;
    509 }
    510 
    511 }  // namespace views
    512