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 screen overflow of the |window_bounds|. 29 int GetOffScreenLength(const gfx::Rect& monitor_bounds, 30 const gfx::Rect& window_bounds, 31 bool vertical) { 32 if (monitor_bounds.IsEmpty() || monitor_bounds.Contains(window_bounds)) 33 return 0; 34 35 // window_bounds 36 // +-------------------------------+ 37 // | top | 38 // | +----------------+ | 39 // | left | monitor_bounds | right | 40 // | +----------------+ | 41 // | bottom | 42 // +-------------------------------+ 43 if (vertical) 44 return std::max(0, monitor_bounds.y() - window_bounds.y()) + 45 std::max(0, window_bounds.bottom() - monitor_bounds.bottom()); 46 return std::max(0, monitor_bounds.x() - window_bounds.x()) + 47 std::max(0, window_bounds.right() - monitor_bounds.right()); 48 } 49 50 } // namespace 51 52 namespace views { 53 54 // static 55 const char BubbleFrameView::kViewClassName[] = "BubbleFrameView"; 56 57 // static 58 gfx::Insets BubbleFrameView::GetTitleInsets() { 59 return gfx::Insets(kTitleTopInset, kTitleLeftInset, kTitleBottomInset, 0); 60 } 61 62 BubbleFrameView::BubbleFrameView(const gfx::Insets& content_margins) 63 : bubble_border_(NULL), 64 content_margins_(content_margins), 65 title_(NULL), 66 close_(NULL), 67 titlebar_extra_view_(NULL) { 68 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 69 title_ = new Label(string16(), rb.GetFont(ui::ResourceBundle::MediumFont)); 70 title_->SetHorizontalAlignment(gfx::ALIGN_LEFT); 71 AddChildView(title_); 72 73 close_ = new LabelButton(this, string16()); 74 close_->SetImage(CustomButton::STATE_NORMAL, 75 *rb.GetImageNamed(IDR_CLOSE_DIALOG).ToImageSkia()); 76 close_->SetImage(CustomButton::STATE_HOVERED, 77 *rb.GetImageNamed(IDR_CLOSE_DIALOG_H).ToImageSkia()); 78 close_->SetImage(CustomButton::STATE_PRESSED, 79 *rb.GetImageNamed(IDR_CLOSE_DIALOG_P).ToImageSkia()); 80 close_->SetSize(close_->GetPreferredSize()); 81 close_->set_border(NULL); 82 close_->SetVisible(false); 83 AddChildView(close_); 84 } 85 86 BubbleFrameView::~BubbleFrameView() {} 87 88 gfx::Rect BubbleFrameView::GetBoundsForClientView() const { 89 gfx::Rect client_bounds = GetLocalBounds(); 90 client_bounds.Inset(GetInsets()); 91 client_bounds.Inset(bubble_border_->GetInsets()); 92 return client_bounds; 93 } 94 95 gfx::Rect BubbleFrameView::GetWindowBoundsForClientBounds( 96 const gfx::Rect& client_bounds) const { 97 return const_cast<BubbleFrameView*>(this)->GetUpdatedWindowBounds( 98 gfx::Rect(), client_bounds.size(), false); 99 } 100 101 int BubbleFrameView::NonClientHitTest(const gfx::Point& point) { 102 if (!bounds().Contains(point)) 103 return HTNOWHERE; 104 if (close_->visible() && close_->GetMirroredBounds().Contains(point)) 105 return HTCLOSE; 106 107 // Allow dialogs to show the system menu and be dragged. 108 if (GetWidget()->widget_delegate()->AsDialogDelegate()) { 109 gfx::Rect sys_rect(0, 0, title_->x(), title_->y()); 110 sys_rect.set_origin(gfx::Point(GetMirroredXForRect(sys_rect), 0)); 111 if (sys_rect.Contains(point)) 112 return HTSYSMENU; 113 if (point.y() < title_->bounds().bottom()) 114 return HTCAPTION; 115 } 116 117 return GetWidget()->client_view()->NonClientHitTest(point); 118 } 119 120 void BubbleFrameView::GetWindowMask(const gfx::Size& size, 121 gfx::Path* window_mask) { 122 // NOTE: this only provides implementations for the types used by dialogs. 123 if ((bubble_border_->arrow() != BubbleBorder::NONE && 124 bubble_border_->arrow() != BubbleBorder::FLOAT) || 125 (bubble_border_->shadow() != BubbleBorder::SMALL_SHADOW && 126 bubble_border_->shadow() != BubbleBorder::NO_SHADOW_OPAQUE_BORDER)) 127 return; 128 129 // Use a window mask roughly matching the border in the image assets. 130 static const int kBorderStrokeSize = 1; 131 static const SkScalar kCornerRadius = SkIntToScalar(6); 132 const gfx::Insets border_insets = bubble_border_->GetInsets(); 133 SkRect rect = { SkIntToScalar(border_insets.left() - kBorderStrokeSize), 134 SkIntToScalar(border_insets.top() - kBorderStrokeSize), 135 SkIntToScalar(size.width() - border_insets.right() + 136 kBorderStrokeSize), 137 SkIntToScalar(size.height() - border_insets.bottom() + 138 kBorderStrokeSize) }; 139 if (bubble_border_->shadow() == BubbleBorder::NO_SHADOW_OPAQUE_BORDER) { 140 window_mask->addRoundRect(rect, kCornerRadius, kCornerRadius); 141 } else { 142 static const int kBottomBorderShadowSize = 2; 143 rect.fBottom += SkIntToScalar(kBottomBorderShadowSize); 144 window_mask->addRect(rect); 145 } 146 } 147 148 void BubbleFrameView::ResetWindowControls() { 149 close_->SetVisible(GetWidget()->widget_delegate()->ShouldShowCloseButton()); 150 } 151 152 void BubbleFrameView::UpdateWindowIcon() {} 153 154 void BubbleFrameView::UpdateWindowTitle() { 155 title_->SetText(GetWidget()->widget_delegate()->ShouldShowWindowTitle() ? 156 GetWidget()->widget_delegate()->GetWindowTitle() : string16()); 157 // Update the close button visibility too, otherwise it's not intialized. 158 ResetWindowControls(); 159 } 160 161 gfx::Insets BubbleFrameView::GetInsets() const { 162 gfx::Insets insets = content_margins_; 163 const int title_height = title_->text().empty() ? 0 : 164 title_->GetPreferredSize().height() + kTitleTopInset + kTitleBottomInset; 165 const int close_height = close_->visible() ? close_->height() : 0; 166 insets += gfx::Insets(std::max(title_height, close_height), 0, 0, 0); 167 return insets; 168 } 169 170 gfx::Size BubbleFrameView::GetPreferredSize() { 171 const gfx::Size client(GetWidget()->client_view()->GetPreferredSize()); 172 gfx::Size size(GetUpdatedWindowBounds(gfx::Rect(), client, false).size()); 173 // Accommodate the width of the title bar elements. 174 int title_bar_width = GetInsets().width() + border()->GetInsets().width(); 175 if (!title_->text().empty()) 176 title_bar_width += kTitleLeftInset + title_->GetPreferredSize().width(); 177 if (close_->visible()) 178 title_bar_width += close_->width() + 1; 179 if (titlebar_extra_view_ != NULL) 180 title_bar_width += titlebar_extra_view_->GetPreferredSize().width(); 181 size.SetToMax(gfx::Size(title_bar_width, 0)); 182 return size; 183 } 184 185 void BubbleFrameView::Layout() { 186 gfx::Rect bounds(GetLocalBounds()); 187 bounds.Inset(border()->GetInsets()); 188 // Small additional insets yield the desired 10px visual close button insets. 189 bounds.Inset(0, 0, close_->width() + 1, 0); 190 close_->SetPosition(gfx::Point(bounds.right(), bounds.y() + 2)); 191 192 gfx::Rect title_bounds(bounds); 193 title_bounds.Inset(kTitleLeftInset, kTitleTopInset, 0, 0); 194 gfx::Size title_size(title_->GetPreferredSize()); 195 const int title_width = std::max(0, close_->bounds().x() - title_bounds.x()); 196 title_size.SetToMin(gfx::Size(title_width, title_size.height())); 197 title_bounds.set_size(title_size); 198 title_->SetBoundsRect(title_bounds); 199 200 if (titlebar_extra_view_) { 201 const int extra_width = close_->bounds().x() - title_->bounds().right(); 202 gfx::Size size = titlebar_extra_view_->GetPreferredSize(); 203 size.SetToMin(gfx::Size(std::max(0, extra_width), size.height())); 204 gfx::Rect titlebar_extra_view_bounds( 205 bounds.right() - size.width(), 206 title_bounds.y(), 207 size.width(), 208 title_bounds.height()); 209 titlebar_extra_view_bounds.Subtract(title_bounds); 210 titlebar_extra_view_->SetBoundsRect(titlebar_extra_view_bounds); 211 } 212 } 213 214 const char* BubbleFrameView::GetClassName() const { 215 return kViewClassName; 216 } 217 218 void BubbleFrameView::ChildPreferredSizeChanged(View* child) { 219 if (child == titlebar_extra_view_ || child == title_) 220 Layout(); 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::GetMonitorBounds(const gfx::Rect& rect) { 270 // TODO(scottmg): Native is wrong. http://crbug.com/133312 271 return gfx::Screen::GetNativeScreen()->GetDisplayNearestPoint( 272 rect.CenterPoint()).work_area(); 273 } 274 275 void BubbleFrameView::MirrorArrowIfOffScreen( 276 bool vertical, 277 const gfx::Rect& anchor_rect, 278 const gfx::Size& client_size) { 279 // Check if the bounds don't fit on screen. 280 gfx::Rect monitor_rect(GetMonitorBounds(anchor_rect)); 281 gfx::Rect window_bounds(bubble_border_->GetBounds(anchor_rect, client_size)); 282 if (GetOffScreenLength(monitor_rect, window_bounds, vertical) > 0) { 283 BubbleBorder::Arrow arrow = bubble_border()->arrow(); 284 // Mirror the arrow and get the new bounds. 285 bubble_border_->set_arrow( 286 vertical ? BubbleBorder::vertical_mirror(arrow) : 287 BubbleBorder::horizontal_mirror(arrow)); 288 gfx::Rect mirror_bounds = 289 bubble_border_->GetBounds(anchor_rect, client_size); 290 // Restore the original arrow if mirroring doesn't show more of the bubble. 291 if (GetOffScreenLength(monitor_rect, mirror_bounds, vertical) >= 292 GetOffScreenLength(monitor_rect, window_bounds, vertical)) 293 bubble_border_->set_arrow(arrow); 294 else 295 SchedulePaint(); 296 } 297 } 298 299 void BubbleFrameView::OffsetArrowIfOffScreen(const gfx::Rect& anchor_rect, 300 const gfx::Size& client_size) { 301 BubbleBorder::Arrow arrow = bubble_border()->arrow(); 302 DCHECK(BubbleBorder::is_arrow_at_center(arrow)); 303 304 // Get the desired bubble bounds without adjustment. 305 bubble_border_->set_arrow_offset(0); 306 gfx::Rect window_bounds(bubble_border_->GetBounds(anchor_rect, client_size)); 307 308 gfx::Rect monitor_rect(GetMonitorBounds(anchor_rect)); 309 if (monitor_rect.IsEmpty() || monitor_rect.Contains(window_bounds)) 310 return; 311 312 // Calculate off-screen adjustment. 313 const bool is_horizontal = BubbleBorder::is_arrow_on_horizontal(arrow); 314 int offscreen_adjust = 0; 315 if (is_horizontal) { 316 if (window_bounds.x() < monitor_rect.x()) 317 offscreen_adjust = monitor_rect.x() - window_bounds.x(); 318 else if (window_bounds.right() > monitor_rect.right()) 319 offscreen_adjust = monitor_rect.right() - window_bounds.right(); 320 } else { 321 if (window_bounds.y() < monitor_rect.y()) 322 offscreen_adjust = monitor_rect.y() - window_bounds.y(); 323 else if (window_bounds.bottom() > monitor_rect.bottom()) 324 offscreen_adjust = monitor_rect.bottom() - window_bounds.bottom(); 325 } 326 327 // For center arrows, arrows are moved in the opposite direction of 328 // |offscreen_adjust|, e.g. positive |offscreen_adjust| means bubble 329 // window needs to be moved to the right and that means we need to move arrow 330 // to the left, and that means negative offset. 331 bubble_border_->set_arrow_offset( 332 bubble_border_->GetArrowOffset(window_bounds.size()) - offscreen_adjust); 333 if (offscreen_adjust) 334 SchedulePaint(); 335 } 336 337 } // namespace views 338