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.
5 #include "ui/views/bubble/bubble_frame_view.h"
9 #include "ui/base/hit_test.h"
10 #include "ui/base/l10n/l10n_util.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/native_theme/native_theme.h"
16 #include "ui/resources/grit/ui_resources.h"
17 #include "ui/strings/grit/ui_strings.h"
18 #include "ui/views/bubble/bubble_border.h"
19 #include "ui/views/controls/button/label_button.h"
20 #include "ui/views/controls/image_view.h"
21 #include "ui/views/resources/grit/views_resources.h"
22 #include "ui/views/widget/widget.h"
23 #include "ui/views/widget/widget_delegate.h"
24 #include "ui/views/window/client_view.h"
28 // Insets for the title bar views in pixels.
29 const int kTitleTopInset
= 12;
30 const int kTitleLeftInset
= 19;
31 const int kTitleBottomInset
= 12;
32 const int kTitleRightInset
= 7;
34 // The horizontal padding between the title and the icon.
35 const int kTitleHorizontalPadding
= 5;
37 // Get the |vertical| or horizontal amount that |available_bounds| overflows
39 int GetOffScreenLength(const gfx::Rect
& available_bounds
,
40 const gfx::Rect
& window_bounds
,
42 if (available_bounds
.IsEmpty() || available_bounds
.Contains(window_bounds
))
46 // +---------------------------------+
48 // | +------------------+ |
49 // | left | available_bounds | right |
50 // | +------------------+ |
52 // +---------------------------------+
54 return std::max(0, available_bounds
.y() - window_bounds
.y()) +
55 std::max(0, window_bounds
.bottom() - available_bounds
.bottom());
56 return std::max(0, available_bounds
.x() - window_bounds
.x()) +
57 std::max(0, window_bounds
.right() - available_bounds
.right());
65 const char BubbleFrameView::kViewClassName
[] = "BubbleFrameView";
67 BubbleFrameView::BubbleFrameView(const gfx::Insets
& content_margins
)
68 : bubble_border_(nullptr),
69 content_margins_(content_margins
),
70 title_icon_(new views::ImageView()),
73 titlebar_extra_view_(nullptr) {
74 AddChildView(title_icon_
);
76 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
77 title_
= new Label(base::string16(),
78 rb
.GetFontList(ui::ResourceBundle::MediumFont
));
79 title_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
80 title_
->set_collapse_when_hidden(true);
81 title_
->SetVisible(false);
84 close_
= CreateCloseButton(this);
85 close_
->SetVisible(false);
89 BubbleFrameView::~BubbleFrameView() {}
92 gfx::Insets
BubbleFrameView::GetTitleInsets() {
94 kTitleTopInset
, kTitleLeftInset
, kTitleBottomInset
, kTitleRightInset
);
98 LabelButton
* BubbleFrameView::CreateCloseButton(ButtonListener
* listener
) {
99 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
100 LabelButton
* close
= new LabelButton(listener
, base::string16());
101 close
->SetImage(CustomButton::STATE_NORMAL
,
102 *rb
.GetImageNamed(IDR_CLOSE_DIALOG
).ToImageSkia());
103 close
->SetImage(CustomButton::STATE_HOVERED
,
104 *rb
.GetImageNamed(IDR_CLOSE_DIALOG_H
).ToImageSkia());
105 close
->SetImage(CustomButton::STATE_PRESSED
,
106 *rb
.GetImageNamed(IDR_CLOSE_DIALOG_P
).ToImageSkia());
107 close
->SetBorder(nullptr);
108 close
->SetSize(close
->GetPreferredSize());
110 // Windows will automatically create a tooltip for the close button based on
111 // the HTCLOSE result from NonClientHitTest().
112 close
->SetTooltipText(l10n_util::GetStringUTF16(IDS_APP_CLOSE
));
117 gfx::Rect
BubbleFrameView::GetBoundsForClientView() const {
118 gfx::Rect client_bounds
= GetLocalBounds();
119 client_bounds
.Inset(GetInsets());
120 client_bounds
.Inset(bubble_border_
->GetInsets());
121 return client_bounds
;
124 gfx::Rect
BubbleFrameView::GetWindowBoundsForClientBounds(
125 const gfx::Rect
& client_bounds
) const {
126 return const_cast<BubbleFrameView
*>(this)->GetUpdatedWindowBounds(
127 gfx::Rect(), client_bounds
.size(), false);
130 int BubbleFrameView::NonClientHitTest(const gfx::Point
& point
) {
131 if (!bounds().Contains(point
))
133 if (close_
->visible() && close_
->GetMirroredBounds().Contains(point
))
136 // Allow dialogs to show the system menu and be dragged.
137 if (GetWidget()->widget_delegate()->AsDialogDelegate()) {
138 gfx::Rect
sys_rect(0, 0, title_
->x(), title_
->y());
139 sys_rect
.set_origin(gfx::Point(GetMirroredXForRect(sys_rect
), 0));
140 if (sys_rect
.Contains(point
))
142 if (point
.y() < title_
->bounds().bottom())
146 return GetWidget()->client_view()->NonClientHitTest(point
);
149 void BubbleFrameView::GetWindowMask(const gfx::Size
& size
,
150 gfx::Path
* window_mask
) {
151 // NOTE: this only provides implementations for the types used by dialogs.
152 if ((bubble_border_
->arrow() != BubbleBorder::NONE
&&
153 bubble_border_
->arrow() != BubbleBorder::FLOAT
) ||
154 (bubble_border_
->shadow() != BubbleBorder::SMALL_SHADOW
&&
155 bubble_border_
->shadow() != BubbleBorder::NO_SHADOW_OPAQUE_BORDER
))
158 // Use a window mask roughly matching the border in the image assets.
159 static const int kBorderStrokeSize
= 1;
160 static const SkScalar kCornerRadius
= SkIntToScalar(6);
161 const gfx::Insets border_insets
= bubble_border_
->GetInsets();
162 SkRect rect
= { SkIntToScalar(border_insets
.left() - kBorderStrokeSize
),
163 SkIntToScalar(border_insets
.top() - kBorderStrokeSize
),
164 SkIntToScalar(size
.width() - border_insets
.right() +
166 SkIntToScalar(size
.height() - border_insets
.bottom() +
167 kBorderStrokeSize
) };
168 if (bubble_border_
->shadow() == BubbleBorder::NO_SHADOW_OPAQUE_BORDER
) {
169 window_mask
->addRoundRect(rect
, kCornerRadius
, kCornerRadius
);
171 static const int kBottomBorderShadowSize
= 2;
172 rect
.fBottom
+= SkIntToScalar(kBottomBorderShadowSize
);
173 window_mask
->addRect(rect
);
177 void BubbleFrameView::ResetWindowControls() {
178 close_
->SetVisible(GetWidget()->widget_delegate()->ShouldShowCloseButton());
181 void BubbleFrameView::UpdateWindowIcon() {
182 gfx::ImageSkia image
;
183 if (GetWidget()->widget_delegate()->ShouldShowWindowIcon())
184 image
= GetWidget()->widget_delegate()->GetWindowIcon();
185 title_icon_
->SetImage(&image
);
189 void BubbleFrameView::UpdateWindowTitle() {
190 title_
->SetText(GetWidget()->widget_delegate()->GetWindowTitle());
191 title_
->SetVisible(GetWidget()->widget_delegate()->ShouldShowWindowTitle());
194 void BubbleFrameView::SizeConstraintsChanged() {}
196 void BubbleFrameView::SetTitleFontList(const gfx::FontList
& font_list
) {
197 title_
->SetFontList(font_list
);
200 gfx::Insets
BubbleFrameView::GetInsets() const {
201 gfx::Insets insets
= content_margins_
;
203 const int icon_height
= title_icon_
->GetPreferredSize().height();
204 const int label_height
= title_
->GetPreferredSize().height();
205 const bool has_title
= icon_height
> 0 || label_height
> 0;
206 const int title_padding
= has_title
? kTitleTopInset
+ kTitleBottomInset
: 0;
207 const int title_height
= std::max(icon_height
, label_height
) + title_padding
;
208 const int close_height
= close_
->visible() ? close_
->height() : 0;
209 insets
+= gfx::Insets(std::max(title_height
, close_height
), 0, 0, 0);
213 gfx::Size
BubbleFrameView::GetPreferredSize() const {
214 // Get the preferred size of the client area.
215 gfx::Size client_size
= GetWidget()->client_view()->GetPreferredSize();
216 // Expand it to include the bubble border and space for the arrow.
217 return GetWindowBoundsForClientBounds(gfx::Rect(client_size
)).size();
220 gfx::Size
BubbleFrameView::GetMinimumSize() const {
221 // Get the minimum size of the client area.
222 gfx::Size client_size
= GetWidget()->client_view()->GetMinimumSize();
223 // Expand it to include the bubble border and space for the arrow.
224 return GetWindowBoundsForClientBounds(gfx::Rect(client_size
)).size();
227 gfx::Size
BubbleFrameView::GetMaximumSize() const {
228 // A bubble should be non-resizable, so its max size is its preferred size.
230 // On Windows, this causes problems, so do not set a maximum size (it doesn't
231 // take the drop shadow area into account, resulting in a too-small window;
232 // see http://crbug.com/506206). This isn't necessary on Windows anyway, since
233 // the OS doesn't give the user controls to resize a bubble.
236 return GetPreferredSize();
240 void BubbleFrameView::Layout() {
241 gfx::Rect
bounds(GetContentsBounds());
242 bounds
.Inset(GetTitleInsets());
243 if (bounds
.IsEmpty())
246 // The close button top inset is actually smaller than the title top inset.
247 close_
->SetPosition(gfx::Point(bounds
.right() - close_
->width(),
250 gfx::Size
title_icon_size(title_icon_
->GetPreferredSize());
251 gfx::Size
title_label_size(title_
->GetPreferredSize());
253 if (title_icon_size
.width() > 0 && title_label_size
.width() > 0)
254 padding
= kTitleHorizontalPadding
;
255 const int title_height
= std::max(title_icon_size
.height(),
256 title_label_size
.height());
258 const int title_icon_width
= std::max(0, close_
->x() - bounds
.x());
259 title_icon_size
.SetToMin(gfx::Size(title_icon_width
, title_height
));
260 gfx::Rect
title_icon_bounds(
261 bounds
.x(), bounds
.y(), title_icon_size
.width(), title_height
);
262 title_icon_
->SetBoundsRect(title_icon_bounds
);
264 const int title_label_x
= title_icon_
->bounds().right() + padding
;
265 const int title_label_width
= std::max(0, close_
->x() - title_label_x
);
266 title_label_size
.SetToMin(gfx::Size(title_label_width
,
267 title_label_size
.height()));
268 gfx::Rect
title_label_bounds(
269 title_label_x
, bounds
.y(), title_label_size
.width(), title_height
);
270 title_
->SetBoundsRect(title_label_bounds
);
273 title_icon_size
.width() + title_label_size
.width() + padding
);
274 bounds
.set_height(title_height
);
276 if (titlebar_extra_view_
) {
277 const int extra_width
= close_
->x() - bounds
.right();
278 gfx::Size size
= titlebar_extra_view_
->GetPreferredSize();
279 size
.SetToMin(gfx::Size(std::max(0, extra_width
), size
.height()));
280 gfx::Rect
titlebar_extra_view_bounds(
281 close_
->x() - size
.width(),
285 titlebar_extra_view_bounds
.Subtract(bounds
);
286 titlebar_extra_view_
->SetBoundsRect(titlebar_extra_view_bounds
);
290 const char* BubbleFrameView::GetClassName() const {
291 return kViewClassName
;
294 void BubbleFrameView::ChildPreferredSizeChanged(View
* child
) {
295 if (child
== titlebar_extra_view_
|| child
== title_
)
299 void BubbleFrameView::OnThemeChanged() {
301 ResetWindowControls();
305 void BubbleFrameView::OnNativeThemeChanged(const ui::NativeTheme
* theme
) {
306 if (bubble_border_
&& bubble_border_
->use_theme_background_color()) {
307 bubble_border_
->set_background_color(GetNativeTheme()->
308 GetSystemColor(ui::NativeTheme::kColorId_DialogBackground
));
313 void BubbleFrameView::ButtonPressed(Button
* sender
, const ui::Event
& event
) {
314 if (sender
== close_
)
315 GetWidget()->Close();
318 void BubbleFrameView::SetBubbleBorder(scoped_ptr
<BubbleBorder
> border
) {
319 bubble_border_
= border
.get();
320 SetBorder(border
.Pass());
322 // Update the background, which relies on the border.
323 set_background(new views::BubbleBackground(bubble_border_
));
326 void BubbleFrameView::SetTitlebarExtraView(View
* view
) {
328 DCHECK(!titlebar_extra_view_
);
330 titlebar_extra_view_
= view
;
333 gfx::Rect
BubbleFrameView::GetUpdatedWindowBounds(const gfx::Rect
& anchor_rect
,
334 gfx::Size client_size
,
335 bool adjust_if_offscreen
) {
336 gfx::Size
size(GetSizeForClientSize(client_size
));
338 const BubbleBorder::Arrow arrow
= bubble_border_
->arrow();
339 if (adjust_if_offscreen
&& BubbleBorder::has_arrow(arrow
)) {
340 // Try to mirror the anchoring if the bubble does not fit on the screen.
341 if (!bubble_border_
->is_arrow_at_center(arrow
)) {
342 MirrorArrowIfOffScreen(true, anchor_rect
, size
);
343 MirrorArrowIfOffScreen(false, anchor_rect
, size
);
345 const bool mirror_vertical
= BubbleBorder::is_arrow_on_horizontal(arrow
);
346 MirrorArrowIfOffScreen(mirror_vertical
, anchor_rect
, size
);
347 OffsetArrowIfOffScreen(anchor_rect
, size
);
351 // Calculate the bounds with the arrow in its updated location and offset.
352 return bubble_border_
->GetBounds(anchor_rect
, size
);
355 gfx::Rect
BubbleFrameView::GetAvailableScreenBounds(const gfx::Rect
& rect
) {
356 // The bubble attempts to fit within the current screen bounds.
357 // TODO(scottmg): Native is wrong. http://crbug.com/133312
358 return gfx::Screen::GetNativeScreen()->GetDisplayNearestPoint(
359 rect
.CenterPoint()).work_area();
362 bool BubbleFrameView::IsCloseButtonVisible() const {
363 return close_
->visible();
366 gfx::Rect
BubbleFrameView::GetCloseButtonMirroredBounds() const {
367 return close_
->GetMirroredBounds();
370 void BubbleFrameView::MirrorArrowIfOffScreen(
372 const gfx::Rect
& anchor_rect
,
373 const gfx::Size
& client_size
) {
374 // Check if the bounds don't fit on screen.
375 gfx::Rect
available_bounds(GetAvailableScreenBounds(anchor_rect
));
376 gfx::Rect
window_bounds(bubble_border_
->GetBounds(anchor_rect
, client_size
));
377 if (GetOffScreenLength(available_bounds
, window_bounds
, vertical
) > 0) {
378 BubbleBorder::Arrow arrow
= bubble_border()->arrow();
379 // Mirror the arrow and get the new bounds.
380 bubble_border_
->set_arrow(
381 vertical
? BubbleBorder::vertical_mirror(arrow
) :
382 BubbleBorder::horizontal_mirror(arrow
));
383 gfx::Rect mirror_bounds
=
384 bubble_border_
->GetBounds(anchor_rect
, client_size
);
385 // Restore the original arrow if mirroring doesn't show more of the bubble.
386 // Otherwise it should invoke parent's Layout() to layout the content based
387 // on the new bubble border.
388 if (GetOffScreenLength(available_bounds
, mirror_bounds
, vertical
) >=
389 GetOffScreenLength(available_bounds
, window_bounds
, vertical
))
390 bubble_border_
->set_arrow(arrow
);
396 void BubbleFrameView::OffsetArrowIfOffScreen(const gfx::Rect
& anchor_rect
,
397 const gfx::Size
& client_size
) {
398 BubbleBorder::Arrow arrow
= bubble_border()->arrow();
399 DCHECK(BubbleBorder::is_arrow_at_center(arrow
));
401 // Get the desired bubble bounds without adjustment.
402 bubble_border_
->set_arrow_offset(0);
403 gfx::Rect
window_bounds(bubble_border_
->GetBounds(anchor_rect
, client_size
));
405 gfx::Rect
available_bounds(GetAvailableScreenBounds(anchor_rect
));
406 if (available_bounds
.IsEmpty() || available_bounds
.Contains(window_bounds
))
409 // Calculate off-screen adjustment.
410 const bool is_horizontal
= BubbleBorder::is_arrow_on_horizontal(arrow
);
411 int offscreen_adjust
= 0;
413 if (window_bounds
.x() < available_bounds
.x())
414 offscreen_adjust
= available_bounds
.x() - window_bounds
.x();
415 else if (window_bounds
.right() > available_bounds
.right())
416 offscreen_adjust
= available_bounds
.right() - window_bounds
.right();
418 if (window_bounds
.y() < available_bounds
.y())
419 offscreen_adjust
= available_bounds
.y() - window_bounds
.y();
420 else if (window_bounds
.bottom() > available_bounds
.bottom())
421 offscreen_adjust
= available_bounds
.bottom() - window_bounds
.bottom();
424 // For center arrows, arrows are moved in the opposite direction of
425 // |offscreen_adjust|, e.g. positive |offscreen_adjust| means bubble
426 // window needs to be moved to the right and that means we need to move arrow
427 // to the left, and that means negative offset.
428 bubble_border_
->set_arrow_offset(
429 bubble_border_
->GetArrowOffset(window_bounds
.size()) - offscreen_adjust
);
430 if (offscreen_adjust
)
434 gfx::Size
BubbleFrameView::GetSizeForClientSize(
435 const gfx::Size
& client_size
) const {
436 // Accommodate the width of the title bar elements.
437 int title_bar_width
= GetInsets().width() + border()->GetInsets().width();
438 gfx::Size title_icon_size
= title_icon_
->GetPreferredSize();
439 gfx::Size title_label_size
= title_
->GetPreferredSize();
440 if (title_icon_size
.width() > 0 || title_label_size
.width() > 0)
441 title_bar_width
+= kTitleLeftInset
;
442 if (title_icon_size
.width() > 0 && title_label_size
.width() > 0)
443 title_bar_width
+= kTitleHorizontalPadding
;
444 title_bar_width
+= title_icon_size
.width();
445 title_bar_width
+= title_label_size
.width();
446 if (close_
->visible())
447 title_bar_width
+= close_
->width() + 1;
448 if (titlebar_extra_view_
!= NULL
)
449 title_bar_width
+= titlebar_extra_view_
->GetPreferredSize().width();
450 gfx::Size
size(client_size
);
451 size
.SetToMax(gfx::Size(title_bar_width
, 0));
452 const gfx::Insets
insets(GetInsets());
453 size
.Enlarge(insets
.width(), insets
.height());