1// Copyright 2013 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 "ash/shelf/overflow_bubble_view.h"
6
7#include <algorithm>
8
9#include "ash/root_window_controller.h"
10#include "ash/shelf/shelf_constants.h"
11#include "ash/shelf/shelf_layout_manager.h"
12#include "ash/shelf/shelf_view.h"
13#include "ash/shell.h"
14#include "ash/shell_window_ids.h"
15#include "ui/events/event.h"
16#include "ui/gfx/insets.h"
17#include "ui/gfx/screen.h"
18#include "ui/views/bubble/bubble_frame_view.h"
19#include "ui/views/widget/widget.h"
20
21namespace ash {
22namespace {
23
24// Max bubble size to screen size ratio.
25const float kMaxBubbleSizeToScreenRatio = 0.5f;
26
27// Inner padding in pixels for shelf view inside bubble.
28const int kPadding = 2;
29
30// Padding space in pixels between ShelfView's left/top edge to its contents.
31const int kShelfViewLeadingInset = 8;
32
33}  // namespace
34
35OverflowBubbleView::OverflowBubbleView()
36    : shelf_view_(NULL) {
37}
38
39OverflowBubbleView::~OverflowBubbleView() {
40}
41
42void OverflowBubbleView::InitOverflowBubble(views::View* anchor,
43                                            ShelfView* shelf_view) {
44  // set_anchor_view needs to be called before GetShelfLayoutManager() can be
45  // called.
46  SetAnchorView(anchor);
47  set_arrow(GetBubbleArrow());
48  set_background(NULL);
49  set_color(SkColorSetARGB(kShelfBackgroundAlpha, 0, 0, 0));
50  set_margins(gfx::Insets(kPadding, kPadding, kPadding, kPadding));
51  // Overflow bubble should not get focus. If it get focus when it is shown,
52  // active state item is changed to running state.
53  set_can_activate(false);
54
55  // Makes bubble view has a layer and clip its children layers.
56  SetPaintToLayer(true);
57  SetFillsBoundsOpaquely(false);
58  layer()->SetMasksToBounds(true);
59
60  shelf_view_ = shelf_view;
61  AddChildView(shelf_view_);
62
63  set_parent_window(Shell::GetContainer(
64      anchor->GetWidget()->GetNativeWindow()->GetRootWindow(),
65      kShellWindowId_ShelfBubbleContainer));
66  views::BubbleDelegateView::CreateBubble(this);
67}
68
69bool OverflowBubbleView::IsHorizontalAlignment() const {
70  ShelfLayoutManager* shelf_layout_manager = GetShelfLayoutManager();
71  return shelf_layout_manager ? shelf_layout_manager->IsHorizontalAlignment()
72                              : false;
73}
74
75const gfx::Size OverflowBubbleView::GetContentsSize() const {
76  return static_cast<views::View*>(shelf_view_)->GetPreferredSize();
77}
78
79// Gets arrow location based on shelf alignment.
80views::BubbleBorder::Arrow OverflowBubbleView::GetBubbleArrow() const {
81  ShelfLayoutManager* shelf_layout_manager = GetShelfLayoutManager();
82  return shelf_layout_manager ?
83      shelf_layout_manager->SelectValueForShelfAlignment(
84          views::BubbleBorder::BOTTOM_LEFT,
85          views::BubbleBorder::LEFT_TOP,
86          views::BubbleBorder::RIGHT_TOP,
87          views::BubbleBorder::TOP_LEFT) :
88      views::BubbleBorder::NONE;
89}
90
91void OverflowBubbleView::ScrollByXOffset(int x_offset) {
92  const gfx::Rect visible_bounds(GetContentsBounds());
93  const gfx::Size contents_size(GetContentsSize());
94
95  DCHECK_GE(contents_size.width(), visible_bounds.width());
96  int x = std::min(contents_size.width() - visible_bounds.width(),
97                   std::max(0, scroll_offset_.x() + x_offset));
98  scroll_offset_.set_x(x);
99}
100
101void OverflowBubbleView::ScrollByYOffset(int y_offset) {
102  const gfx::Rect visible_bounds(GetContentsBounds());
103  const gfx::Size contents_size(GetContentsSize());
104
105  DCHECK_GE(contents_size.width(), visible_bounds.width());
106  int y = std::min(contents_size.height() - visible_bounds.height(),
107                   std::max(0, scroll_offset_.y() + y_offset));
108  scroll_offset_.set_y(y);
109}
110
111gfx::Size OverflowBubbleView::GetPreferredSize() const {
112  gfx::Size preferred_size = GetContentsSize();
113
114  const gfx::Rect monitor_rect = Shell::GetScreen()->GetDisplayNearestPoint(
115      GetAnchorRect().CenterPoint()).work_area();
116  if (!monitor_rect.IsEmpty()) {
117    if (IsHorizontalAlignment()) {
118      preferred_size.set_width(std::min(
119          preferred_size.width(),
120          static_cast<int>(monitor_rect.width() *
121                           kMaxBubbleSizeToScreenRatio)));
122    } else {
123      preferred_size.set_height(std::min(
124          preferred_size.height(),
125          static_cast<int>(monitor_rect.height() *
126                           kMaxBubbleSizeToScreenRatio)));
127    }
128  }
129
130  return preferred_size;
131}
132
133void OverflowBubbleView::Layout() {
134  shelf_view_->SetBoundsRect(gfx::Rect(
135      gfx::PointAtOffsetFromOrigin(-scroll_offset_), GetContentsSize()));
136}
137
138void OverflowBubbleView::ChildPreferredSizeChanged(views::View* child) {
139  // When contents size is changed, ContentsBounds should be updated before
140  // calculating scroll offset.
141  SizeToContents();
142
143  // Ensures |shelf_view_| is still visible.
144  if (IsHorizontalAlignment())
145    ScrollByXOffset(0);
146  else
147    ScrollByYOffset(0);
148  Layout();
149}
150
151bool OverflowBubbleView::OnMouseWheel(const ui::MouseWheelEvent& event) {
152  // The MouseWheelEvent was changed to support both X and Y offsets
153  // recently, but the behavior of this function was retained to continue
154  // using Y offsets only. Might be good to simply scroll in both
155  // directions as in OverflowBubbleView::OnScrollEvent.
156  if (IsHorizontalAlignment())
157    ScrollByXOffset(-event.y_offset());
158  else
159    ScrollByYOffset(-event.y_offset());
160  Layout();
161
162  return true;
163}
164
165ShelfLayoutManager* OverflowBubbleView::GetShelfLayoutManager() const {
166  return GetAnchorView() ? ShelfLayoutManager::ForShelf(
167                               GetAnchorView()->GetWidget()->GetNativeView())
168                         : NULL;
169}
170
171void OverflowBubbleView::OnScrollEvent(ui::ScrollEvent* event) {
172  ScrollByXOffset(-event->x_offset());
173  ScrollByYOffset(-event->y_offset());
174  Layout();
175  event->SetHandled();
176}
177
178gfx::Rect OverflowBubbleView::GetBubbleBounds() {
179  views::BubbleBorder* border = GetBubbleFrameView()->bubble_border();
180  gfx::Insets bubble_insets = border->GetInsets();
181
182  const int border_size =
183      views::BubbleBorder::is_arrow_on_horizontal(arrow()) ?
184      bubble_insets.left() : bubble_insets.top();
185  const int arrow_offset = border_size + kPadding + kShelfViewLeadingInset +
186      kShelfSize / 2;
187
188  const gfx::Size content_size = GetPreferredSize();
189  border->set_arrow_offset(arrow_offset);
190
191  const gfx::Rect anchor_rect = GetAnchorRect();
192  gfx::Rect bubble_rect = GetBubbleFrameView()->GetUpdatedWindowBounds(
193      anchor_rect,
194      content_size,
195      false);
196
197  gfx::Rect monitor_rect = Shell::GetScreen()->GetDisplayNearestPoint(
198      anchor_rect.CenterPoint()).work_area();
199
200  int offset = 0;
201  if (views::BubbleBorder::is_arrow_on_horizontal(arrow())) {
202    if (bubble_rect.x() < monitor_rect.x())
203      offset = monitor_rect.x() - bubble_rect.x();
204    else if (bubble_rect.right() > monitor_rect.right())
205      offset = monitor_rect.right() - bubble_rect.right();
206
207    bubble_rect.Offset(offset, 0);
208    border->set_arrow_offset(anchor_rect.CenterPoint().x() - bubble_rect.x());
209  } else {
210    if (bubble_rect.y() < monitor_rect.y())
211      offset = monitor_rect.y() - bubble_rect.y();
212    else if (bubble_rect.bottom() > monitor_rect.bottom())
213      offset =  monitor_rect.bottom() - bubble_rect.bottom();
214
215    bubble_rect.Offset(0, offset);
216    border->set_arrow_offset(anchor_rect.CenterPoint().y() - bubble_rect.y());
217  }
218
219  GetBubbleFrameView()->SchedulePaint();
220  return bubble_rect;
221}
222
223}  // namespace ash
224