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