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/bubble/bubble_frame_view.h"
6
7#include <algorithm>
8
9#include "ui/base/hit_test.h"
10#include "ui/base/resource/resource_bundle.h"
11#include "ui/gfx/path.h"
12#include "ui/gfx/screen.h"
13#include "ui/gfx/skia_util.h"
14#include "ui/native_theme/native_theme.h"
15#include "ui/resources/grit/ui_resources.h"
16#include "ui/views/bubble/bubble_border.h"
17#include "ui/views/controls/button/label_button.h"
18#include "ui/views/widget/widget.h"
19#include "ui/views/widget/widget_delegate.h"
20#include "ui/views/window/client_view.h"
21
22namespace {
23
24// Insets for the title bar views in pixels.
25const int kTitleTopInset = 12;
26const int kTitleLeftInset = 19;
27const int kTitleBottomInset = 12;
28const int kTitleRightInset = 7;
29
30// Get the |vertical| or horizontal amount that |available_bounds| overflows
31// |window_bounds|.
32int GetOffScreenLength(const gfx::Rect& available_bounds,
33                       const gfx::Rect& window_bounds,
34                       bool vertical) {
35  if (available_bounds.IsEmpty() || available_bounds.Contains(window_bounds))
36    return 0;
37
38  //  window_bounds
39  //  +---------------------------------+
40  //  |             top                 |
41  //  |      +------------------+       |
42  //  | left | available_bounds | right |
43  //  |      +------------------+       |
44  //  |            bottom               |
45  //  +---------------------------------+
46  if (vertical)
47    return std::max(0, available_bounds.y() - window_bounds.y()) +
48           std::max(0, window_bounds.bottom() - available_bounds.bottom());
49  return std::max(0, available_bounds.x() - window_bounds.x()) +
50         std::max(0, window_bounds.right() - available_bounds.right());
51}
52
53}  // namespace
54
55namespace views {
56
57// static
58const char BubbleFrameView::kViewClassName[] = "BubbleFrameView";
59
60// static
61gfx::Insets BubbleFrameView::GetTitleInsets() {
62  return gfx::Insets(kTitleTopInset, kTitleLeftInset,
63                     kTitleBottomInset, kTitleRightInset);
64}
65
66BubbleFrameView::BubbleFrameView(const gfx::Insets& content_margins)
67    : bubble_border_(NULL),
68      content_margins_(content_margins),
69      title_(NULL),
70      close_(NULL),
71      titlebar_extra_view_(NULL) {
72  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
73  title_ = new Label(base::string16(),
74                     rb.GetFontList(ui::ResourceBundle::MediumFont));
75  title_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
76  AddChildView(title_);
77
78  close_ = new LabelButton(this, base::string16());
79  close_->SetImage(CustomButton::STATE_NORMAL,
80                   *rb.GetImageNamed(IDR_CLOSE_DIALOG).ToImageSkia());
81  close_->SetImage(CustomButton::STATE_HOVERED,
82                   *rb.GetImageNamed(IDR_CLOSE_DIALOG_H).ToImageSkia());
83  close_->SetImage(CustomButton::STATE_PRESSED,
84                   *rb.GetImageNamed(IDR_CLOSE_DIALOG_P).ToImageSkia());
85  close_->SetBorder(scoped_ptr<Border>());
86  close_->SetSize(close_->GetPreferredSize());
87  close_->SetVisible(false);
88  AddChildView(close_);
89}
90
91BubbleFrameView::~BubbleFrameView() {}
92
93gfx::Rect BubbleFrameView::GetBoundsForClientView() const {
94  gfx::Rect client_bounds = GetLocalBounds();
95  client_bounds.Inset(GetInsets());
96  client_bounds.Inset(bubble_border_->GetInsets());
97  return client_bounds;
98}
99
100gfx::Rect BubbleFrameView::GetWindowBoundsForClientBounds(
101    const gfx::Rect& client_bounds) const {
102  return const_cast<BubbleFrameView*>(this)->GetUpdatedWindowBounds(
103      gfx::Rect(), client_bounds.size(), false);
104}
105
106int BubbleFrameView::NonClientHitTest(const gfx::Point& point) {
107  if (!bounds().Contains(point))
108    return HTNOWHERE;
109  if (close_->visible() && close_->GetMirroredBounds().Contains(point))
110    return HTCLOSE;
111
112  // Allow dialogs to show the system menu and be dragged.
113  if (GetWidget()->widget_delegate()->AsDialogDelegate()) {
114    gfx::Rect sys_rect(0, 0, title_->x(), title_->y());
115    sys_rect.set_origin(gfx::Point(GetMirroredXForRect(sys_rect), 0));
116    if (sys_rect.Contains(point))
117      return HTSYSMENU;
118    if (point.y() < title_->bounds().bottom())
119      return HTCAPTION;
120  }
121
122  return GetWidget()->client_view()->NonClientHitTest(point);
123}
124
125void BubbleFrameView::GetWindowMask(const gfx::Size& size,
126                                    gfx::Path* window_mask) {
127  // NOTE: this only provides implementations for the types used by dialogs.
128  if ((bubble_border_->arrow() != BubbleBorder::NONE &&
129       bubble_border_->arrow() != BubbleBorder::FLOAT) ||
130      (bubble_border_->shadow() != BubbleBorder::SMALL_SHADOW &&
131       bubble_border_->shadow() != BubbleBorder::NO_SHADOW_OPAQUE_BORDER))
132    return;
133
134  // Use a window mask roughly matching the border in the image assets.
135  static const int kBorderStrokeSize = 1;
136  static const SkScalar kCornerRadius = SkIntToScalar(6);
137  const gfx::Insets border_insets = bubble_border_->GetInsets();
138  SkRect rect = { SkIntToScalar(border_insets.left() - kBorderStrokeSize),
139                  SkIntToScalar(border_insets.top() - kBorderStrokeSize),
140                  SkIntToScalar(size.width() - border_insets.right() +
141                                kBorderStrokeSize),
142                  SkIntToScalar(size.height() - border_insets.bottom() +
143                                kBorderStrokeSize) };
144  if (bubble_border_->shadow() == BubbleBorder::NO_SHADOW_OPAQUE_BORDER) {
145    window_mask->addRoundRect(rect, kCornerRadius, kCornerRadius);
146  } else {
147    static const int kBottomBorderShadowSize = 2;
148    rect.fBottom += SkIntToScalar(kBottomBorderShadowSize);
149    window_mask->addRect(rect);
150  }
151}
152
153void BubbleFrameView::ResetWindowControls() {
154  close_->SetVisible(GetWidget()->widget_delegate()->ShouldShowCloseButton());
155}
156
157void BubbleFrameView::UpdateWindowIcon() {}
158
159void BubbleFrameView::UpdateWindowTitle() {
160  title_->SetText(GetWidget()->widget_delegate()->ShouldShowWindowTitle() ?
161      GetWidget()->widget_delegate()->GetWindowTitle() : base::string16());
162  // Update the close button visibility too, otherwise it's not intialized.
163  ResetWindowControls();
164}
165
166void BubbleFrameView::SizeConstraintsChanged() {}
167
168void BubbleFrameView::SetTitleFontList(const gfx::FontList& font_list) {
169  title_->SetFontList(font_list);
170}
171
172gfx::Insets BubbleFrameView::GetInsets() const {
173  gfx::Insets insets = content_margins_;
174  const int title_height = title_->text().empty() ? 0 :
175      title_->GetPreferredSize().height() + kTitleTopInset + kTitleBottomInset;
176  const int close_height = close_->visible() ? close_->height() : 0;
177  insets += gfx::Insets(std::max(title_height, close_height), 0, 0, 0);
178  return insets;
179}
180
181gfx::Size BubbleFrameView::GetPreferredSize() const {
182  return GetSizeForClientSize(GetWidget()->client_view()->GetPreferredSize());
183}
184
185gfx::Size BubbleFrameView::GetMinimumSize() const {
186  return GetSizeForClientSize(GetWidget()->client_view()->GetMinimumSize());
187}
188
189void BubbleFrameView::Layout() {
190  gfx::Rect bounds(GetContentsBounds());
191  bounds.Inset(GetTitleInsets());
192  if (bounds.IsEmpty())
193    return;
194
195  // The close button top inset is actually smaller than the title top inset.
196  close_->SetPosition(gfx::Point(bounds.right() - close_->width(),
197                                 bounds.y() - 5));
198
199  gfx::Size title_size(title_->GetPreferredSize());
200  const int title_width = std::max(0, close_->x() - bounds.x());
201  title_size.SetToMin(gfx::Size(title_width, title_size.height()));
202  bounds.set_size(title_size);
203  title_->SetBoundsRect(bounds);
204
205  if (titlebar_extra_view_) {
206    const int extra_width = close_->x() - title_->bounds().right();
207    gfx::Size size = titlebar_extra_view_->GetPreferredSize();
208    size.SetToMin(gfx::Size(std::max(0, extra_width), size.height()));
209    gfx::Rect titlebar_extra_view_bounds(
210        close_->x() - size.width(),
211        bounds.y(),
212        size.width(),
213        bounds.height());
214    titlebar_extra_view_bounds.Subtract(bounds);
215    titlebar_extra_view_->SetBoundsRect(titlebar_extra_view_bounds);
216  }
217}
218
219const char* BubbleFrameView::GetClassName() const {
220  return kViewClassName;
221}
222
223void BubbleFrameView::ChildPreferredSizeChanged(View* child) {
224  if (child == titlebar_extra_view_ || child == title_)
225    Layout();
226}
227
228void BubbleFrameView::OnThemeChanged() {
229  UpdateWindowTitle();
230  ResetWindowControls();
231  UpdateWindowIcon();
232}
233
234void BubbleFrameView::OnNativeThemeChanged(const ui::NativeTheme* theme) {
235  if (bubble_border_ && bubble_border_->use_theme_background_color()) {
236    bubble_border_->set_background_color(GetNativeTheme()->
237        GetSystemColor(ui::NativeTheme::kColorId_DialogBackground));
238    SchedulePaint();
239  }
240}
241
242void BubbleFrameView::ButtonPressed(Button* sender, const ui::Event& event) {
243  if (sender == close_)
244    GetWidget()->Close();
245}
246
247void BubbleFrameView::SetBubbleBorder(scoped_ptr<BubbleBorder> border) {
248  bubble_border_ = border.get();
249  SetBorder(border.PassAs<Border>());
250
251  // Update the background, which relies on the border.
252  set_background(new views::BubbleBackground(bubble_border_));
253}
254
255void BubbleFrameView::SetTitlebarExtraView(View* view) {
256  DCHECK(view);
257  DCHECK(!titlebar_extra_view_);
258  AddChildView(view);
259  titlebar_extra_view_ = view;
260}
261
262gfx::Rect BubbleFrameView::GetUpdatedWindowBounds(const gfx::Rect& anchor_rect,
263                                                  gfx::Size client_size,
264                                                  bool adjust_if_offscreen) {
265  gfx::Size size(GetSizeForClientSize(client_size));
266
267  const BubbleBorder::Arrow arrow = bubble_border_->arrow();
268  if (adjust_if_offscreen && BubbleBorder::has_arrow(arrow)) {
269    // Try to mirror the anchoring if the bubble does not fit on the screen.
270    if (!bubble_border_->is_arrow_at_center(arrow)) {
271      MirrorArrowIfOffScreen(true, anchor_rect, size);
272      MirrorArrowIfOffScreen(false, anchor_rect, size);
273    } else {
274      const bool mirror_vertical = BubbleBorder::is_arrow_on_horizontal(arrow);
275      MirrorArrowIfOffScreen(mirror_vertical, anchor_rect, size);
276      OffsetArrowIfOffScreen(anchor_rect, size);
277    }
278  }
279
280  // Calculate the bounds with the arrow in its updated location and offset.
281  return bubble_border_->GetBounds(anchor_rect, size);
282}
283
284gfx::Rect BubbleFrameView::GetAvailableScreenBounds(const gfx::Rect& rect) {
285  // The bubble attempts to fit within the current screen bounds.
286  // TODO(scottmg): Native is wrong. http://crbug.com/133312
287  return gfx::Screen::GetNativeScreen()->GetDisplayNearestPoint(
288      rect.CenterPoint()).work_area();
289}
290
291void BubbleFrameView::MirrorArrowIfOffScreen(
292    bool vertical,
293    const gfx::Rect& anchor_rect,
294    const gfx::Size& client_size) {
295  // Check if the bounds don't fit on screen.
296  gfx::Rect available_bounds(GetAvailableScreenBounds(anchor_rect));
297  gfx::Rect window_bounds(bubble_border_->GetBounds(anchor_rect, client_size));
298  if (GetOffScreenLength(available_bounds, window_bounds, vertical) > 0) {
299    BubbleBorder::Arrow arrow = bubble_border()->arrow();
300    // Mirror the arrow and get the new bounds.
301    bubble_border_->set_arrow(
302        vertical ? BubbleBorder::vertical_mirror(arrow) :
303                   BubbleBorder::horizontal_mirror(arrow));
304    gfx::Rect mirror_bounds =
305        bubble_border_->GetBounds(anchor_rect, client_size);
306    // Restore the original arrow if mirroring doesn't show more of the bubble.
307    // Otherwise it should invoke parent's Layout() to layout the content based
308    // on the new bubble border.
309    if (GetOffScreenLength(available_bounds, mirror_bounds, vertical) >=
310        GetOffScreenLength(available_bounds, window_bounds, vertical))
311      bubble_border_->set_arrow(arrow);
312    else if (parent())
313      parent()->Layout();
314  }
315}
316
317void BubbleFrameView::OffsetArrowIfOffScreen(const gfx::Rect& anchor_rect,
318                                             const gfx::Size& client_size) {
319  BubbleBorder::Arrow arrow = bubble_border()->arrow();
320  DCHECK(BubbleBorder::is_arrow_at_center(arrow));
321
322  // Get the desired bubble bounds without adjustment.
323  bubble_border_->set_arrow_offset(0);
324  gfx::Rect window_bounds(bubble_border_->GetBounds(anchor_rect, client_size));
325
326  gfx::Rect available_bounds(GetAvailableScreenBounds(anchor_rect));
327  if (available_bounds.IsEmpty() || available_bounds.Contains(window_bounds))
328    return;
329
330  // Calculate off-screen adjustment.
331  const bool is_horizontal = BubbleBorder::is_arrow_on_horizontal(arrow);
332  int offscreen_adjust = 0;
333  if (is_horizontal) {
334    if (window_bounds.x() < available_bounds.x())
335      offscreen_adjust = available_bounds.x() - window_bounds.x();
336    else if (window_bounds.right() > available_bounds.right())
337      offscreen_adjust = available_bounds.right() - window_bounds.right();
338  } else {
339    if (window_bounds.y() < available_bounds.y())
340      offscreen_adjust = available_bounds.y() - window_bounds.y();
341    else if (window_bounds.bottom() > available_bounds.bottom())
342      offscreen_adjust = available_bounds.bottom() - window_bounds.bottom();
343  }
344
345  // For center arrows, arrows are moved in the opposite direction of
346  // |offscreen_adjust|, e.g. positive |offscreen_adjust| means bubble
347  // window needs to be moved to the right and that means we need to move arrow
348  // to the left, and that means negative offset.
349  bubble_border_->set_arrow_offset(
350      bubble_border_->GetArrowOffset(window_bounds.size()) - offscreen_adjust);
351  if (offscreen_adjust)
352    SchedulePaint();
353}
354
355gfx::Size BubbleFrameView::GetSizeForClientSize(
356    const gfx::Size& client_size) const {
357  // Accommodate the width of the title bar elements.
358  int title_bar_width = GetInsets().width() + border()->GetInsets().width();
359  if (!title_->text().empty())
360    title_bar_width += kTitleLeftInset + title_->GetPreferredSize().width();
361  if (close_->visible())
362    title_bar_width += close_->width() + 1;
363  if (titlebar_extra_view_ != NULL)
364    title_bar_width += titlebar_extra_view_->GetPreferredSize().width();
365  gfx::Size size(client_size);
366  size.SetToMax(gfx::Size(title_bar_width, 0));
367  const gfx::Insets insets(GetInsets());
368  size.Enlarge(insets.width(), insets.height());
369  return size;
370}
371
372}  // namespace views
373