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