1 // Copyright 2014 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 "apps/ui/views/app_window_frame_view.h" 6 7 #include "base/strings/utf_string_conversions.h" 8 #include "extensions/browser/app_window/native_app_window.h" 9 #include "extensions/common/draggable_region.h" 10 #include "grit/theme_resources.h" 11 #include "third_party/skia/include/core/SkPaint.h" 12 #include "third_party/skia/include/core/SkRegion.h" 13 #include "ui/base/hit_test.h" 14 #include "ui/base/l10n/l10n_util.h" 15 #include "ui/base/resource/resource_bundle.h" 16 #include "ui/gfx/canvas.h" 17 #include "ui/gfx/color_utils.h" 18 #include "ui/gfx/image/image.h" 19 #include "ui/gfx/path.h" 20 #include "ui/strings/grit/ui_strings.h" // Accessibility names 21 #include "ui/views/controls/button/image_button.h" 22 #include "ui/views/layout/grid_layout.h" 23 #include "ui/views/views_delegate.h" 24 #include "ui/views/widget/widget.h" 25 #include "ui/views/widget/widget_delegate.h" 26 27 namespace { 28 29 const int kDefaultResizeInsideBoundsSize = 5; 30 const int kDefaultResizeAreaCornerSize = 16; 31 const int kCaptionHeight = 25; 32 33 } // namespace 34 35 namespace apps { 36 37 const char AppWindowFrameView::kViewClassName[] = 38 "browser/ui/views/extensions/AppWindowFrameView"; 39 40 AppWindowFrameView::AppWindowFrameView(views::Widget* widget, 41 extensions::NativeAppWindow* window, 42 bool draw_frame, 43 const SkColor& active_frame_color, 44 const SkColor& inactive_frame_color) 45 : widget_(widget), 46 window_(window), 47 draw_frame_(draw_frame), 48 active_frame_color_(active_frame_color), 49 inactive_frame_color_(inactive_frame_color), 50 close_button_(NULL), 51 maximize_button_(NULL), 52 restore_button_(NULL), 53 minimize_button_(NULL), 54 resize_inside_bounds_size_(kDefaultResizeInsideBoundsSize), 55 resize_outside_bounds_size_(0), 56 resize_area_corner_size_(kDefaultResizeAreaCornerSize) { 57 } 58 59 AppWindowFrameView::~AppWindowFrameView() {} 60 61 void AppWindowFrameView::Init() { 62 if (draw_frame_) { 63 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 64 close_button_ = new views::ImageButton(this); 65 close_button_->SetImage( 66 views::CustomButton::STATE_NORMAL, 67 rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE).ToImageSkia()); 68 close_button_->SetImage( 69 views::CustomButton::STATE_HOVERED, 70 rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE_H).ToImageSkia()); 71 close_button_->SetImage( 72 views::CustomButton::STATE_PRESSED, 73 rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE_P).ToImageSkia()); 74 close_button_->SetAccessibleName( 75 l10n_util::GetStringUTF16(IDS_APP_ACCNAME_CLOSE)); 76 AddChildView(close_button_); 77 // STATE_NORMAL images are set in SetButtonImagesForFrame, not here. 78 maximize_button_ = new views::ImageButton(this); 79 maximize_button_->SetImage( 80 views::CustomButton::STATE_HOVERED, 81 rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_H).ToImageSkia()); 82 maximize_button_->SetImage( 83 views::CustomButton::STATE_PRESSED, 84 rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_P).ToImageSkia()); 85 maximize_button_->SetImage( 86 views::CustomButton::STATE_DISABLED, 87 rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_D).ToImageSkia()); 88 maximize_button_->SetAccessibleName( 89 l10n_util::GetStringUTF16(IDS_APP_ACCNAME_MAXIMIZE)); 90 AddChildView(maximize_button_); 91 restore_button_ = new views::ImageButton(this); 92 restore_button_->SetImage( 93 views::CustomButton::STATE_HOVERED, 94 rb.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE_H).ToImageSkia()); 95 restore_button_->SetImage( 96 views::CustomButton::STATE_PRESSED, 97 rb.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE_P).ToImageSkia()); 98 restore_button_->SetAccessibleName( 99 l10n_util::GetStringUTF16(IDS_APP_ACCNAME_RESTORE)); 100 AddChildView(restore_button_); 101 minimize_button_ = new views::ImageButton(this); 102 minimize_button_->SetImage( 103 views::CustomButton::STATE_HOVERED, 104 rb.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE_H).ToImageSkia()); 105 minimize_button_->SetImage( 106 views::CustomButton::STATE_PRESSED, 107 rb.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE_P).ToImageSkia()); 108 minimize_button_->SetAccessibleName( 109 l10n_util::GetStringUTF16(IDS_APP_ACCNAME_MINIMIZE)); 110 AddChildView(minimize_button_); 111 112 SetButtonImagesForFrame(); 113 } 114 } 115 116 void AppWindowFrameView::SetResizeSizes(int resize_inside_bounds_size, 117 int resize_outside_bounds_size, 118 int resize_area_corner_size) { 119 resize_inside_bounds_size_ = resize_inside_bounds_size; 120 resize_outside_bounds_size_ = resize_outside_bounds_size; 121 resize_area_corner_size_ = resize_area_corner_size; 122 } 123 124 // views::NonClientFrameView implementation. 125 126 gfx::Rect AppWindowFrameView::GetBoundsForClientView() const { 127 if (!draw_frame_ || widget_->IsFullscreen()) 128 return bounds(); 129 return gfx::Rect( 130 0, kCaptionHeight, width(), std::max(0, height() - kCaptionHeight)); 131 } 132 133 gfx::Rect AppWindowFrameView::GetWindowBoundsForClientBounds( 134 const gfx::Rect& client_bounds) const { 135 gfx::Rect window_bounds = client_bounds; 136 #if defined(OS_LINUX) && !defined(OS_CHROMEOS) 137 // Get the difference between the widget's client area bounds and window 138 // bounds, and grow |window_bounds| by that amount. 139 gfx::Insets native_frame_insets = 140 widget_->GetClientAreaBoundsInScreen().InsetsFrom( 141 widget_->GetWindowBoundsInScreen()); 142 window_bounds.Inset(native_frame_insets); 143 #endif 144 if (!draw_frame_) { 145 // Enforce minimum size (1, 1) in case that client_bounds is passed with 146 // empty size. This could occur when the frameless window is being 147 // initialized. 148 if (window_bounds.IsEmpty()) { 149 window_bounds.set_width(1); 150 window_bounds.set_height(1); 151 } 152 return window_bounds; 153 } 154 155 int closeButtonOffsetX = (kCaptionHeight - close_button_->height()) / 2; 156 int header_width = close_button_->width() + closeButtonOffsetX * 2; 157 return gfx::Rect(window_bounds.x(), 158 window_bounds.y() - kCaptionHeight, 159 std::max(header_width, window_bounds.width()), 160 window_bounds.height() + kCaptionHeight); 161 } 162 163 int AppWindowFrameView::NonClientHitTest(const gfx::Point& point) { 164 if (widget_->IsFullscreen()) 165 return HTCLIENT; 166 167 gfx::Rect expanded_bounds = bounds(); 168 if (resize_outside_bounds_size_) { 169 expanded_bounds.Inset(gfx::Insets(-resize_outside_bounds_size_, 170 -resize_outside_bounds_size_, 171 -resize_outside_bounds_size_, 172 -resize_outside_bounds_size_)); 173 } 174 // Points outside the (possibly expanded) bounds can be discarded. 175 if (!expanded_bounds.Contains(point)) 176 return HTNOWHERE; 177 178 // Check the frame first, as we allow a small area overlapping the contents 179 // to be used for resize handles. 180 bool can_ever_resize = widget_->widget_delegate() 181 ? widget_->widget_delegate()->CanResize() 182 : false; 183 // Don't allow overlapping resize handles when the window is maximized or 184 // fullscreen, as it can't be resized in those states. 185 int resize_border = (widget_->IsMaximized() || widget_->IsFullscreen()) 186 ? 0 187 : resize_inside_bounds_size_; 188 int frame_component = GetHTComponentForFrame(point, 189 resize_border, 190 resize_border, 191 resize_area_corner_size_, 192 resize_area_corner_size_, 193 can_ever_resize); 194 if (frame_component != HTNOWHERE) 195 return frame_component; 196 197 // Check for possible draggable region in the client area for the frameless 198 // window. 199 SkRegion* draggable_region = window_->GetDraggableRegion(); 200 if (draggable_region && draggable_region->contains(point.x(), point.y())) 201 return HTCAPTION; 202 203 int client_component = widget_->client_view()->NonClientHitTest(point); 204 if (client_component != HTNOWHERE) 205 return client_component; 206 207 // Then see if the point is within any of the window controls. 208 if (close_button_ && close_button_->visible() && 209 close_button_->GetMirroredBounds().Contains(point)) { 210 return HTCLOSE; 211 } 212 if ((maximize_button_ && maximize_button_->visible() && 213 maximize_button_->GetMirroredBounds().Contains(point)) || 214 (restore_button_ && restore_button_->visible() && 215 restore_button_->GetMirroredBounds().Contains(point))) { 216 return HTMAXBUTTON; 217 } 218 if (minimize_button_ && minimize_button_->visible() && 219 minimize_button_->GetMirroredBounds().Contains(point)) { 220 return HTMINBUTTON; 221 } 222 223 // Caption is a safe default. 224 return HTCAPTION; 225 } 226 227 void AppWindowFrameView::GetWindowMask(const gfx::Size& size, 228 gfx::Path* window_mask) { 229 // We got nothing to say about no window mask. 230 } 231 232 void AppWindowFrameView::SizeConstraintsChanged() { 233 if (draw_frame_) { 234 maximize_button_->SetEnabled(widget_->widget_delegate() && 235 widget_->widget_delegate()->CanMaximize()); 236 } 237 } 238 239 gfx::Size AppWindowFrameView::GetPreferredSize() const { 240 gfx::Size pref = widget_->client_view()->GetPreferredSize(); 241 gfx::Rect bounds(0, 0, pref.width(), pref.height()); 242 return widget_->non_client_view() 243 ->GetWindowBoundsForClientBounds(bounds) 244 .size(); 245 } 246 247 void AppWindowFrameView::Layout() { 248 if (!draw_frame_) 249 return; 250 gfx::Size close_size = close_button_->GetPreferredSize(); 251 const int kButtonOffsetY = 0; 252 const int kButtonSpacing = 1; 253 const int kRightMargin = 3; 254 255 close_button_->SetBounds(width() - kRightMargin - close_size.width(), 256 kButtonOffsetY, 257 close_size.width(), 258 close_size.height()); 259 260 maximize_button_->SetEnabled(widget_->widget_delegate() && 261 widget_->widget_delegate()->CanMaximize()); 262 gfx::Size maximize_size = maximize_button_->GetPreferredSize(); 263 maximize_button_->SetBounds( 264 close_button_->x() - kButtonSpacing - maximize_size.width(), 265 kButtonOffsetY, 266 maximize_size.width(), 267 maximize_size.height()); 268 gfx::Size restore_size = restore_button_->GetPreferredSize(); 269 restore_button_->SetBounds( 270 close_button_->x() - kButtonSpacing - restore_size.width(), 271 kButtonOffsetY, 272 restore_size.width(), 273 restore_size.height()); 274 275 bool maximized = widget_->IsMaximized(); 276 maximize_button_->SetVisible(!maximized); 277 restore_button_->SetVisible(maximized); 278 if (maximized) 279 maximize_button_->SetState(views::CustomButton::STATE_NORMAL); 280 else 281 restore_button_->SetState(views::CustomButton::STATE_NORMAL); 282 283 gfx::Size minimize_size = minimize_button_->GetPreferredSize(); 284 minimize_button_->SetState(views::CustomButton::STATE_NORMAL); 285 minimize_button_->SetBounds( 286 maximize_button_->x() - kButtonSpacing - minimize_size.width(), 287 kButtonOffsetY, 288 minimize_size.width(), 289 minimize_size.height()); 290 } 291 292 void AppWindowFrameView::OnPaint(gfx::Canvas* canvas) { 293 if (!draw_frame_) 294 return; 295 296 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 297 if (ShouldPaintAsActive()) { 298 close_button_->SetImage( 299 views::CustomButton::STATE_NORMAL, 300 rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE).ToImageSkia()); 301 } else { 302 close_button_->SetImage( 303 views::CustomButton::STATE_NORMAL, 304 rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE_U).ToImageSkia()); 305 } 306 307 SetButtonImagesForFrame(); 308 // TODO(benwells): different look for inactive by default. 309 SkPaint paint; 310 paint.setAntiAlias(false); 311 paint.setStyle(SkPaint::kFill_Style); 312 paint.setColor(CurrentFrameColor()); 313 gfx::Path path; 314 path.moveTo(0, 0); 315 path.lineTo(width(), 0); 316 path.lineTo(width(), kCaptionHeight); 317 path.lineTo(0, kCaptionHeight); 318 path.close(); 319 canvas->DrawPath(path, paint); 320 } 321 322 const char* AppWindowFrameView::GetClassName() const { return kViewClassName; } 323 324 gfx::Size AppWindowFrameView::GetMinimumSize() const { 325 gfx::Size min_size = widget_->client_view()->GetMinimumSize(); 326 if (!draw_frame_) { 327 min_size.SetToMax(gfx::Size(1, 1)); 328 return min_size; 329 } 330 331 // Ensure we can display the top of the caption area. 332 gfx::Rect client_bounds = GetBoundsForClientView(); 333 min_size.Enlarge(0, client_bounds.y()); 334 // Ensure we have enough space for the window icon and buttons. We allow 335 // the title string to collapse to zero width. 336 int closeButtonOffsetX = (kCaptionHeight - close_button_->height()) / 2; 337 int header_width = close_button_->width() + closeButtonOffsetX * 2; 338 if (header_width > min_size.width()) 339 min_size.set_width(header_width); 340 return min_size; 341 } 342 343 gfx::Size AppWindowFrameView::GetMaximumSize() const { 344 gfx::Size max_size = widget_->client_view()->GetMaximumSize(); 345 346 // Add to the client maximum size the height of any title bar and borders. 347 gfx::Size client_size = GetBoundsForClientView().size(); 348 if (max_size.width()) 349 max_size.Enlarge(width() - client_size.width(), 0); 350 if (max_size.height()) 351 max_size.Enlarge(0, height() - client_size.height()); 352 353 return max_size; 354 } 355 356 void AppWindowFrameView::ButtonPressed(views::Button* sender, 357 const ui::Event& event) { 358 DCHECK(draw_frame_); 359 if (sender == close_button_) 360 widget_->Close(); 361 else if (sender == maximize_button_) 362 widget_->Maximize(); 363 else if (sender == restore_button_) 364 widget_->Restore(); 365 else if (sender == minimize_button_) 366 widget_->Minimize(); 367 } 368 369 SkColor AppWindowFrameView::CurrentFrameColor() { 370 return widget_->IsActive() ? active_frame_color_ : inactive_frame_color_; 371 } 372 373 void AppWindowFrameView::SetButtonImagesForFrame() { 374 DCHECK(draw_frame_); 375 376 // If the frame is dark, we should use the light images so they have 377 // some contrast. 378 unsigned char frame_luma = 379 color_utils::GetLuminanceForColor(CurrentFrameColor()); 380 const unsigned char kLuminanceThreshold = 100; 381 bool use_light = frame_luma < kLuminanceThreshold; 382 383 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 384 if (use_light) { 385 maximize_button_->SetImage( 386 views::CustomButton::STATE_NORMAL, 387 rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_L).ToImageSkia()); 388 restore_button_->SetImage( 389 views::CustomButton::STATE_NORMAL, 390 rb.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE_L).ToImageSkia()); 391 minimize_button_->SetImage( 392 views::CustomButton::STATE_NORMAL, 393 rb.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE_L).ToImageSkia()); 394 } else { 395 maximize_button_->SetImage( 396 views::CustomButton::STATE_NORMAL, 397 rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE).ToImageSkia()); 398 restore_button_->SetImage( 399 views::CustomButton::STATE_NORMAL, 400 rb.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE).ToImageSkia()); 401 minimize_button_->SetImage( 402 views::CustomButton::STATE_NORMAL, 403 rb.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE).ToImageSkia()); 404 } 405 } 406 407 } // namespace apps 408