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