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|. 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 59 gfx::Insets BubbleFrameView::GetTitleInsets() { 60 return gfx::Insets(kTitleTopInset, kTitleLeftInset, kTitleBottomInset, 0); 61 } 62 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 87 BubbleFrameView::~BubbleFrameView() {} 88 89 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 96 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 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 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 149 void BubbleFrameView::ResetWindowControls() { 150 close_->SetVisible(GetWidget()->widget_delegate()->ShouldShowCloseButton()); 151 } 152 153 void BubbleFrameView::UpdateWindowIcon() {} 154 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 162 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 171 gfx::Size BubbleFrameView::GetPreferredSize() { 172 return GetSizeForClientSize(GetWidget()->client_view()->GetPreferredSize()); 173 } 174 175 gfx::Size BubbleFrameView::GetMinimumSize() { 176 return GetSizeForClientSize(GetWidget()->client_view()->GetMinimumSize()); 177 } 178 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 208 const char* BubbleFrameView::GetClassName() const { 209 return kViewClassName; 210 } 211 212 void BubbleFrameView::ChildPreferredSizeChanged(View* child) { 213 if (child == titlebar_extra_view_ || child == title_) 214 Layout(); 215 } 216 217 void BubbleFrameView::OnThemeChanged() { 218 UpdateWindowTitle(); 219 ResetWindowControls(); 220 UpdateWindowIcon(); 221 } 222 223 void BubbleFrameView::ButtonPressed(Button* sender, const ui::Event& event) { 224 if (sender == close_) 225 GetWidget()->Close(); 226 } 227 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 236 void BubbleFrameView::SetTitlebarExtraView(View* view) { 237 DCHECK(view); 238 DCHECK(!titlebar_extra_view_); 239 AddChildView(view); 240 titlebar_extra_view_ = view; 241 } 242 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 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 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 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 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