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 22 namespace { 23 24 // Insets for the title bar views in pixels. 25 const int kTitleTopInset = 12; 26 const int kTitleLeftInset = 19; 27 const int kTitleBottomInset = 12; 28 const int kTitleRightInset = 7; 29 30 // Get the |vertical| or horizontal amount that |available_bounds| overflows 31 // |window_bounds|. 32 int 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 55 namespace views { 56 57 // static 58 const char BubbleFrameView::kViewClassName[] = "BubbleFrameView"; 59 60 // static 61 gfx::Insets BubbleFrameView::GetTitleInsets() { 62 return gfx::Insets(kTitleTopInset, kTitleLeftInset, 63 kTitleBottomInset, kTitleRightInset); 64 } 65 66 BubbleFrameView::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 91 BubbleFrameView::~BubbleFrameView() {} 92 93 gfx::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 100 gfx::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 106 int 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 125 void 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 153 void BubbleFrameView::ResetWindowControls() { 154 close_->SetVisible(GetWidget()->widget_delegate()->ShouldShowCloseButton()); 155 } 156 157 void BubbleFrameView::UpdateWindowIcon() {} 158 159 void 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 166 void BubbleFrameView::SizeConstraintsChanged() {} 167 168 void BubbleFrameView::SetTitleFontList(const gfx::FontList& font_list) { 169 title_->SetFontList(font_list); 170 } 171 172 gfx::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 181 gfx::Size BubbleFrameView::GetPreferredSize() const { 182 return GetSizeForClientSize(GetWidget()->client_view()->GetPreferredSize()); 183 } 184 185 gfx::Size BubbleFrameView::GetMinimumSize() const { 186 return GetSizeForClientSize(GetWidget()->client_view()->GetMinimumSize()); 187 } 188 189 void 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 219 const char* BubbleFrameView::GetClassName() const { 220 return kViewClassName; 221 } 222 223 void BubbleFrameView::ChildPreferredSizeChanged(View* child) { 224 if (child == titlebar_extra_view_ || child == title_) 225 Layout(); 226 } 227 228 void BubbleFrameView::OnThemeChanged() { 229 UpdateWindowTitle(); 230 ResetWindowControls(); 231 UpdateWindowIcon(); 232 } 233 234 void 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 242 void BubbleFrameView::ButtonPressed(Button* sender, const ui::Event& event) { 243 if (sender == close_) 244 GetWidget()->Close(); 245 } 246 247 void 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 255 void BubbleFrameView::SetTitlebarExtraView(View* view) { 256 DCHECK(view); 257 DCHECK(!titlebar_extra_view_); 258 AddChildView(view); 259 titlebar_extra_view_ = view; 260 } 261 262 gfx::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 284 gfx::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 291 void 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 317 void 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 355 gfx::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