blob: 2d3db74126ae32a37b33e27240ffcc3aec9cc843 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/views/controls/menu/menu_scroll_view_container.h"
#include "third_party/skia/include/core/SkPaint.h"
#include "third_party/skia/include/core/SkPath.h"
#include "ui/accessibility/ax_view_state.h"
#include "ui/gfx/canvas.h"
#include "ui/native_theme/native_theme_aura.h"
#include "ui/views/border.h"
#include "ui/views/bubble/bubble_border.h"
#include "ui/views/controls/menu/menu_config.h"
#include "ui/views/controls/menu/menu_controller.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/submenu_view.h"
#include "ui/views/round_rect_painter.h"
using ui::NativeTheme;
namespace views {
namespace {
static const int kBorderPaddingDueToRoundedCorners = 1;
// MenuScrollButton ------------------------------------------------------------
// MenuScrollButton is used for the scroll buttons when not all menu items fit
// on screen. MenuScrollButton forwards appropriate events to the
// MenuController.
class MenuScrollButton : public View {
public:
MenuScrollButton(SubmenuView* host, bool is_up)
: host_(host),
is_up_(is_up),
// Make our height the same as that of other MenuItemViews.
pref_height_(MenuItemView::pref_menu_height()) {
}
virtual gfx::Size GetPreferredSize() const override {
return gfx::Size(
host_->GetMenuItem()->GetMenuConfig().scroll_arrow_height * 2 - 1,
pref_height_);
}
virtual bool CanDrop(const OSExchangeData& data) override {
DCHECK(host_->GetMenuItem()->GetMenuController());
return true; // Always return true so that drop events are targeted to us.
}
virtual void OnDragEntered(const ui::DropTargetEvent& event) override {
DCHECK(host_->GetMenuItem()->GetMenuController());
host_->GetMenuItem()->GetMenuController()->OnDragEnteredScrollButton(
host_, is_up_);
}
virtual int OnDragUpdated(const ui::DropTargetEvent& event) override {
return ui::DragDropTypes::DRAG_NONE;
}
virtual void OnDragExited() override {
DCHECK(host_->GetMenuItem()->GetMenuController());
host_->GetMenuItem()->GetMenuController()->OnDragExitedScrollButton(host_);
}
virtual int OnPerformDrop(const ui::DropTargetEvent& event) override {
return ui::DragDropTypes::DRAG_NONE;
}
virtual void OnPaint(gfx::Canvas* canvas) override {
const MenuConfig& config = host_->GetMenuItem()->GetMenuConfig();
// The background.
gfx::Rect item_bounds(0, 0, width(), height());
NativeTheme::ExtraParams extra;
extra.menu_item.is_selected = false;
GetNativeTheme()->Paint(canvas->sk_canvas(),
NativeTheme::kMenuItemBackground,
NativeTheme::kNormal, item_bounds, extra);
// Then the arrow.
int x = width() / 2;
int y = (height() - config.scroll_arrow_height) / 2;
int x_left = x - config.scroll_arrow_height;
int x_right = x + config.scroll_arrow_height;
int y_bottom;
if (!is_up_) {
y_bottom = y;
y = y_bottom + config.scroll_arrow_height;
} else {
y_bottom = y + config.scroll_arrow_height;
}
SkPath path;
path.setFillType(SkPath::kWinding_FillType);
path.moveTo(SkIntToScalar(x), SkIntToScalar(y));
path.lineTo(SkIntToScalar(x_left), SkIntToScalar(y_bottom));
path.lineTo(SkIntToScalar(x_right), SkIntToScalar(y_bottom));
path.lineTo(SkIntToScalar(x), SkIntToScalar(y));
SkPaint paint;
paint.setStyle(SkPaint::kFill_Style);
paint.setAntiAlias(true);
paint.setColor(config.arrow_color);
canvas->DrawPath(path, paint);
}
private:
// SubmenuView we were created for.
SubmenuView* host_;
// Direction of the button.
bool is_up_;
// Preferred height.
int pref_height_;
DISALLOW_COPY_AND_ASSIGN(MenuScrollButton);
};
} // namespace
// MenuScrollView --------------------------------------------------------------
// MenuScrollView is a viewport for the SubmenuView. It's reason to exist is so
// that ScrollRectToVisible works.
//
// NOTE: It is possible to use ScrollView directly (after making it deal with
// null scrollbars), but clicking on a child of ScrollView forces the window to
// become active, which we don't want. As we really only need a fraction of
// what ScrollView does, so we use a one off variant.
class MenuScrollViewContainer::MenuScrollView : public View {
public:
explicit MenuScrollView(View* child) {
AddChildView(child);
}
virtual void ScrollRectToVisible(const gfx::Rect& rect) override {
// NOTE: this assumes we only want to scroll in the y direction.
// If the rect is already visible, do not scroll.
if (GetLocalBounds().Contains(rect))
return;
// Scroll just enough so that the rect is visible.
int dy = 0;
if (rect.bottom() > GetLocalBounds().bottom())
dy = rect.bottom() - GetLocalBounds().bottom();
else
dy = rect.y();
// Convert rect.y() to view's coordinates and make sure we don't show past
// the bottom of the view.
View* child = GetContents();
child->SetY(-std::max(0, std::min(
child->GetPreferredSize().height() - this->height(),
dy - child->y())));
}
// Returns the contents, which is the SubmenuView.
View* GetContents() {
return child_at(0);
}
private:
DISALLOW_COPY_AND_ASSIGN(MenuScrollView);
};
// MenuScrollViewContainer ----------------------------------------------------
MenuScrollViewContainer::MenuScrollViewContainer(SubmenuView* content_view)
: content_view_(content_view),
arrow_(BubbleBorder::NONE),
bubble_border_(NULL) {
scroll_up_button_ = new MenuScrollButton(content_view, true);
scroll_down_button_ = new MenuScrollButton(content_view, false);
AddChildView(scroll_up_button_);
AddChildView(scroll_down_button_);
scroll_view_ = new MenuScrollView(content_view);
AddChildView(scroll_view_);
arrow_ = BubbleBorderTypeFromAnchor(
content_view_->GetMenuItem()->GetMenuController()->GetAnchorPosition());
if (arrow_ != BubbleBorder::NONE)
CreateBubbleBorder();
else
CreateDefaultBorder();
}
bool MenuScrollViewContainer::HasBubbleBorder() {
return arrow_ != BubbleBorder::NONE;
}
void MenuScrollViewContainer::SetBubbleArrowOffset(int offset) {
DCHECK(HasBubbleBorder());
bubble_border_->set_arrow_offset(offset);
}
void MenuScrollViewContainer::OnPaintBackground(gfx::Canvas* canvas) {
if (background()) {
View::OnPaintBackground(canvas);
return;
}
gfx::Rect bounds(0, 0, width(), height());
NativeTheme::ExtraParams extra;
const MenuConfig& menu_config = content_view_->GetMenuItem()->GetMenuConfig();
extra.menu_background.corner_radius = menu_config.corner_radius;
GetNativeTheme()->Paint(canvas->sk_canvas(),
NativeTheme::kMenuPopupBackground, NativeTheme::kNormal, bounds, extra);
}
void MenuScrollViewContainer::Layout() {
gfx::Insets insets = GetInsets();
int x = insets.left();
int y = insets.top();
int width = View::width() - insets.width();
int content_height = height() - insets.height();
if (!scroll_up_button_->visible()) {
scroll_view_->SetBounds(x, y, width, content_height);
scroll_view_->Layout();
return;
}
gfx::Size pref = scroll_up_button_->GetPreferredSize();
scroll_up_button_->SetBounds(x, y, width, pref.height());
content_height -= pref.height();
const int scroll_view_y = y + pref.height();
pref = scroll_down_button_->GetPreferredSize();
scroll_down_button_->SetBounds(x, height() - pref.height() - insets.top(),
width, pref.height());
content_height -= pref.height();
scroll_view_->SetBounds(x, scroll_view_y, width, content_height);
scroll_view_->Layout();
}
gfx::Size MenuScrollViewContainer::GetPreferredSize() const {
gfx::Size prefsize = scroll_view_->GetContents()->GetPreferredSize();
gfx::Insets insets = GetInsets();
prefsize.Enlarge(insets.width(), insets.height());
return prefsize;
}
void MenuScrollViewContainer::GetAccessibleState(
ui::AXViewState* state) {
// Get the name from the submenu view.
content_view_->GetAccessibleState(state);
// Now change the role.
state->role = ui::AX_ROLE_MENU_BAR;
// Some AT (like NVDA) will not process focus events on menu item children
// unless a parent claims to be focused.
state->AddStateFlag(ui::AX_STATE_FOCUSED);
}
void MenuScrollViewContainer::OnBoundsChanged(
const gfx::Rect& previous_bounds) {
gfx::Size content_pref = scroll_view_->GetContents()->GetPreferredSize();
scroll_up_button_->SetVisible(content_pref.height() > height());
scroll_down_button_->SetVisible(content_pref.height() > height());
Layout();
}
void MenuScrollViewContainer::CreateDefaultBorder() {
arrow_ = BubbleBorder::NONE;
bubble_border_ = NULL;
const MenuConfig& menu_config =
content_view_->GetMenuItem()->GetMenuConfig();
bool use_border = true;
int padding = menu_config.corner_radius > 0 ?
kBorderPaddingDueToRoundedCorners : 0;
#if defined(USE_AURA) && !(defined(OS_LINUX) && !defined(OS_CHROMEOS))
if (menu_config.native_theme == ui::NativeThemeAura::instance()) {
// In case of NativeThemeAura the border gets drawn with the shadow.
// Furthermore no additional padding is wanted.
use_border = false;
padding = 0;
}
#endif
int top = menu_config.menu_vertical_border_size + padding;
int left = menu_config.menu_horizontal_border_size + padding;
int bottom = menu_config.menu_vertical_border_size + padding;
int right = menu_config.menu_horizontal_border_size + padding;
if (use_border) {
SetBorder(views::Border::CreateBorderPainter(
new views::RoundRectPainter(
menu_config.native_theme->GetSystemColor(
ui::NativeTheme::kColorId_MenuBorderColor),
menu_config.corner_radius),
gfx::Insets(top, left, bottom, right)));
} else {
SetBorder(Border::CreateEmptyBorder(top, left, bottom, right));
}
}
void MenuScrollViewContainer::CreateBubbleBorder() {
bubble_border_ = new BubbleBorder(arrow_,
BubbleBorder::SMALL_SHADOW,
SK_ColorWHITE);
SetBorder(scoped_ptr<Border>(bubble_border_));
set_background(new BubbleBackground(bubble_border_));
}
BubbleBorder::Arrow MenuScrollViewContainer::BubbleBorderTypeFromAnchor(
MenuAnchorPosition anchor) {
switch (anchor) {
case MENU_ANCHOR_BUBBLE_LEFT:
return BubbleBorder::RIGHT_CENTER;
case MENU_ANCHOR_BUBBLE_RIGHT:
return BubbleBorder::LEFT_CENTER;
case MENU_ANCHOR_BUBBLE_ABOVE:
return BubbleBorder::BOTTOM_CENTER;
case MENU_ANCHOR_BUBBLE_BELOW:
return BubbleBorder::TOP_CENTER;
default:
return BubbleBorder::NONE;
}
}
} // namespace views