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