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/menu_scroll_view_container.h"
6
7#include "third_party/skia/include/core/SkPaint.h"
8#include "third_party/skia/include/core/SkPath.h"
9#include "ui/accessibility/ax_view_state.h"
10#include "ui/gfx/canvas.h"
11#include "ui/native_theme/native_theme_aura.h"
12#include "ui/views/border.h"
13#include "ui/views/bubble/bubble_border.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_item_view.h"
17#include "ui/views/controls/menu/submenu_view.h"
18#include "ui/views/round_rect_painter.h"
19
20using ui::NativeTheme;
21
22namespace views {
23
24namespace {
25
26static const int kBorderPaddingDueToRoundedCorners = 1;
27
28// MenuScrollButton ------------------------------------------------------------
29
30// MenuScrollButton is used for the scroll buttons when not all menu items fit
31// on screen. MenuScrollButton forwards appropriate events to the
32// MenuController.
33
34class MenuScrollButton : public View {
35 public:
36  MenuScrollButton(SubmenuView* host, bool is_up)
37      : host_(host),
38        is_up_(is_up),
39        // Make our height the same as that of other MenuItemViews.
40        pref_height_(MenuItemView::pref_menu_height()) {
41  }
42
43  virtual gfx::Size GetPreferredSize() const OVERRIDE {
44    return gfx::Size(
45        host_->GetMenuItem()->GetMenuConfig().scroll_arrow_height * 2 - 1,
46        pref_height_);
47  }
48
49  virtual bool CanDrop(const OSExchangeData& data) OVERRIDE {
50    DCHECK(host_->GetMenuItem()->GetMenuController());
51    return true;  // Always return true so that drop events are targeted to us.
52  }
53
54  virtual void OnDragEntered(const ui::DropTargetEvent& event) OVERRIDE {
55    DCHECK(host_->GetMenuItem()->GetMenuController());
56    host_->GetMenuItem()->GetMenuController()->OnDragEnteredScrollButton(
57        host_, is_up_);
58  }
59
60  virtual int OnDragUpdated(const ui::DropTargetEvent& event) OVERRIDE {
61    return ui::DragDropTypes::DRAG_NONE;
62  }
63
64  virtual void OnDragExited() OVERRIDE {
65    DCHECK(host_->GetMenuItem()->GetMenuController());
66    host_->GetMenuItem()->GetMenuController()->OnDragExitedScrollButton(host_);
67  }
68
69  virtual int OnPerformDrop(const ui::DropTargetEvent& event) OVERRIDE {
70    return ui::DragDropTypes::DRAG_NONE;
71  }
72
73  virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE {
74    const MenuConfig& config = host_->GetMenuItem()->GetMenuConfig();
75
76    // The background.
77    gfx::Rect item_bounds(0, 0, width(), height());
78    NativeTheme::ExtraParams extra;
79    extra.menu_item.is_selected = false;
80    GetNativeTheme()->Paint(canvas->sk_canvas(),
81                            NativeTheme::kMenuItemBackground,
82                            NativeTheme::kNormal, item_bounds, extra);
83
84    // Then the arrow.
85    int x = width() / 2;
86    int y = (height() - config.scroll_arrow_height) / 2;
87
88    int x_left = x - config.scroll_arrow_height;
89    int x_right = x + config.scroll_arrow_height;
90    int y_bottom;
91
92    if (!is_up_) {
93      y_bottom = y;
94      y = y_bottom + config.scroll_arrow_height;
95    } else {
96      y_bottom = y + config.scroll_arrow_height;
97    }
98    SkPath path;
99    path.setFillType(SkPath::kWinding_FillType);
100    path.moveTo(SkIntToScalar(x), SkIntToScalar(y));
101    path.lineTo(SkIntToScalar(x_left), SkIntToScalar(y_bottom));
102    path.lineTo(SkIntToScalar(x_right), SkIntToScalar(y_bottom));
103    path.lineTo(SkIntToScalar(x), SkIntToScalar(y));
104    SkPaint paint;
105    paint.setStyle(SkPaint::kFill_Style);
106    paint.setAntiAlias(true);
107    paint.setColor(config.arrow_color);
108    canvas->DrawPath(path, paint);
109  }
110
111 private:
112  // SubmenuView we were created for.
113  SubmenuView* host_;
114
115  // Direction of the button.
116  bool is_up_;
117
118  // Preferred height.
119  int pref_height_;
120
121  DISALLOW_COPY_AND_ASSIGN(MenuScrollButton);
122};
123
124}  // namespace
125
126// MenuScrollView --------------------------------------------------------------
127
128// MenuScrollView is a viewport for the SubmenuView. It's reason to exist is so
129// that ScrollRectToVisible works.
130//
131// NOTE: It is possible to use ScrollView directly (after making it deal with
132// null scrollbars), but clicking on a child of ScrollView forces the window to
133// become active, which we don't want. As we really only need a fraction of
134// what ScrollView does, so we use a one off variant.
135
136class MenuScrollViewContainer::MenuScrollView : public View {
137 public:
138  explicit MenuScrollView(View* child) {
139    AddChildView(child);
140  }
141
142  virtual void ScrollRectToVisible(const gfx::Rect& rect) OVERRIDE {
143    // NOTE: this assumes we only want to scroll in the y direction.
144
145    // If the rect is already visible, do not scroll.
146    if (GetLocalBounds().Contains(rect))
147      return;
148
149    // Scroll just enough so that the rect is visible.
150    int dy = 0;
151    if (rect.bottom() > GetLocalBounds().bottom())
152      dy = rect.bottom() - GetLocalBounds().bottom();
153    else
154      dy = rect.y();
155
156    // Convert rect.y() to view's coordinates and make sure we don't show past
157    // the bottom of the view.
158    View* child = GetContents();
159    child->SetY(-std::max(0, std::min(
160        child->GetPreferredSize().height() - this->height(),
161        dy - child->y())));
162  }
163
164  // Returns the contents, which is the SubmenuView.
165  View* GetContents() {
166    return child_at(0);
167  }
168
169 private:
170  DISALLOW_COPY_AND_ASSIGN(MenuScrollView);
171};
172
173// MenuScrollViewContainer ----------------------------------------------------
174
175MenuScrollViewContainer::MenuScrollViewContainer(SubmenuView* content_view)
176    : content_view_(content_view),
177      arrow_(BubbleBorder::NONE),
178      bubble_border_(NULL) {
179  scroll_up_button_ = new MenuScrollButton(content_view, true);
180  scroll_down_button_ = new MenuScrollButton(content_view, false);
181  AddChildView(scroll_up_button_);
182  AddChildView(scroll_down_button_);
183
184  scroll_view_ = new MenuScrollView(content_view);
185  AddChildView(scroll_view_);
186
187  arrow_ = BubbleBorderTypeFromAnchor(
188      content_view_->GetMenuItem()->GetMenuController()->GetAnchorPosition());
189
190  if (arrow_ != BubbleBorder::NONE)
191    CreateBubbleBorder();
192  else
193    CreateDefaultBorder();
194}
195
196bool MenuScrollViewContainer::HasBubbleBorder() {
197  return arrow_ != BubbleBorder::NONE;
198}
199
200void MenuScrollViewContainer::SetBubbleArrowOffset(int offset) {
201  DCHECK(HasBubbleBorder());
202  bubble_border_->set_arrow_offset(offset);
203}
204
205void MenuScrollViewContainer::OnPaintBackground(gfx::Canvas* canvas) {
206  if (background()) {
207    View::OnPaintBackground(canvas);
208    return;
209  }
210
211  gfx::Rect bounds(0, 0, width(), height());
212  NativeTheme::ExtraParams extra;
213  const MenuConfig& menu_config = content_view_->GetMenuItem()->GetMenuConfig();
214  extra.menu_background.corner_radius = menu_config.corner_radius;
215  GetNativeTheme()->Paint(canvas->sk_canvas(),
216      NativeTheme::kMenuPopupBackground, NativeTheme::kNormal, bounds, extra);
217}
218
219void MenuScrollViewContainer::Layout() {
220  gfx::Insets insets = GetInsets();
221  int x = insets.left();
222  int y = insets.top();
223  int width = View::width() - insets.width();
224  int content_height = height() - insets.height();
225  if (!scroll_up_button_->visible()) {
226    scroll_view_->SetBounds(x, y, width, content_height);
227    scroll_view_->Layout();
228    return;
229  }
230
231  gfx::Size pref = scroll_up_button_->GetPreferredSize();
232  scroll_up_button_->SetBounds(x, y, width, pref.height());
233  content_height -= pref.height();
234
235  const int scroll_view_y = y + pref.height();
236
237  pref = scroll_down_button_->GetPreferredSize();
238  scroll_down_button_->SetBounds(x, height() - pref.height() - insets.top(),
239                                 width, pref.height());
240  content_height -= pref.height();
241
242  scroll_view_->SetBounds(x, scroll_view_y, width, content_height);
243  scroll_view_->Layout();
244}
245
246gfx::Size MenuScrollViewContainer::GetPreferredSize() const {
247  gfx::Size prefsize = scroll_view_->GetContents()->GetPreferredSize();
248  gfx::Insets insets = GetInsets();
249  prefsize.Enlarge(insets.width(), insets.height());
250  return prefsize;
251}
252
253void MenuScrollViewContainer::GetAccessibleState(
254    ui::AXViewState* state) {
255  // Get the name from the submenu view.
256  content_view_->GetAccessibleState(state);
257
258  // Now change the role.
259  state->role = ui::AX_ROLE_MENU_BAR;
260  // Some AT (like NVDA) will not process focus events on menu item children
261  // unless a parent claims to be focused.
262  state->AddStateFlag(ui::AX_STATE_FOCUSED);
263}
264
265void MenuScrollViewContainer::OnBoundsChanged(
266    const gfx::Rect& previous_bounds) {
267  gfx::Size content_pref = scroll_view_->GetContents()->GetPreferredSize();
268  scroll_up_button_->SetVisible(content_pref.height() > height());
269  scroll_down_button_->SetVisible(content_pref.height() > height());
270  Layout();
271}
272
273void MenuScrollViewContainer::CreateDefaultBorder() {
274  arrow_ = BubbleBorder::NONE;
275  bubble_border_ = NULL;
276
277  const MenuConfig& menu_config =
278      content_view_->GetMenuItem()->GetMenuConfig();
279
280  bool use_border = true;
281  int padding = menu_config.corner_radius > 0 ?
282        kBorderPaddingDueToRoundedCorners : 0;
283
284#if defined(USE_AURA) && !(defined(OS_LINUX) && !defined(OS_CHROMEOS))
285  if (menu_config.native_theme == ui::NativeThemeAura::instance()) {
286    // In case of NativeThemeAura the border gets drawn with the shadow.
287    // Furthermore no additional padding is wanted.
288    use_border = false;
289    padding = 0;
290  }
291#endif
292
293  int top = menu_config.menu_vertical_border_size + padding;
294  int left = menu_config.menu_horizontal_border_size + padding;
295  int bottom = menu_config.menu_vertical_border_size + padding;
296  int right = menu_config.menu_horizontal_border_size + padding;
297
298  if (use_border) {
299    SetBorder(views::Border::CreateBorderPainter(
300        new views::RoundRectPainter(
301            menu_config.native_theme->GetSystemColor(
302                ui::NativeTheme::kColorId_MenuBorderColor),
303            menu_config.corner_radius),
304        gfx::Insets(top, left, bottom, right)));
305  } else {
306    SetBorder(Border::CreateEmptyBorder(top, left, bottom, right));
307  }
308}
309
310void MenuScrollViewContainer::CreateBubbleBorder() {
311  bubble_border_ = new BubbleBorder(arrow_,
312                                    BubbleBorder::SMALL_SHADOW,
313                                    SK_ColorWHITE);
314  SetBorder(scoped_ptr<Border>(bubble_border_));
315  set_background(new BubbleBackground(bubble_border_));
316}
317
318BubbleBorder::Arrow MenuScrollViewContainer::BubbleBorderTypeFromAnchor(
319    MenuAnchorPosition anchor) {
320  switch (anchor) {
321    case MENU_ANCHOR_BUBBLE_LEFT:
322      return BubbleBorder::RIGHT_CENTER;
323    case MENU_ANCHOR_BUBBLE_RIGHT:
324      return BubbleBorder::LEFT_CENTER;
325    case MENU_ANCHOR_BUBBLE_ABOVE:
326      return BubbleBorder::BOTTOM_CENTER;
327    case MENU_ANCHOR_BUBBLE_BELOW:
328      return BubbleBorder::TOP_CENTER;
329    default:
330      return BubbleBorder::NONE;
331  }
332}
333
334}  // namespace views
335