1 // Copyright 2013 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/shell_window_frame_view.h" 6 7 #include "apps/ui/native_app_window.h" 8 #include "base/strings/utf_string_conversions.h" 9 #include "extensions/common/draggable_region.h" 10 #include "grit/theme_resources.h" 11 #include "grit/ui_strings.h" // Accessibility names 12 #include "third_party/skia/include/core/SkPaint.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/image/image.h" 18 #include "ui/gfx/path.h" 19 #include "ui/views/controls/button/image_button.h" 20 #include "ui/views/layout/grid_layout.h" 21 #include "ui/views/views_delegate.h" 22 #include "ui/views/widget/widget.h" 23 #include "ui/views/widget/widget_delegate.h" 24 25 #if defined(USE_AURA) 26 #include "ui/aura/env.h" 27 #include "ui/aura/window.h" 28 #endif 29 30 namespace { 31 // Height of the chrome-style caption, in pixels. 32 const int kCaptionHeight = 25; 33 } // namespace 34 35 namespace apps { 36 37 const char ShellWindowFrameView::kViewClassName[] = 38 "browser/ui/views/extensions/ShellWindowFrameView"; 39 40 ShellWindowFrameView::ShellWindowFrameView(NativeAppWindow* window) 41 : window_(window), 42 frame_(NULL), 43 close_button_(NULL), 44 maximize_button_(NULL), 45 restore_button_(NULL), 46 minimize_button_(NULL), 47 resize_inside_bounds_size_(0), 48 resize_area_corner_size_(0) { 49 } 50 51 ShellWindowFrameView::~ShellWindowFrameView() { 52 } 53 54 void ShellWindowFrameView::Init(views::Widget* frame, 55 int resize_inside_bounds_size, 56 int resize_outside_bounds_size, 57 int resize_outside_scale_for_touch, 58 int resize_area_corner_size) { 59 frame_ = frame; 60 resize_inside_bounds_size_ = resize_inside_bounds_size; 61 resize_area_corner_size_ = resize_area_corner_size; 62 63 if (!window_->IsFrameless()) { 64 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 65 close_button_ = new views::ImageButton(this); 66 close_button_->SetImage(views::CustomButton::STATE_NORMAL, 67 rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE).ToImageSkia()); 68 close_button_->SetImage(views::CustomButton::STATE_HOVERED, 69 rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE_H).ToImageSkia()); 70 close_button_->SetImage(views::CustomButton::STATE_PRESSED, 71 rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE_P).ToImageSkia()); 72 close_button_->SetAccessibleName( 73 l10n_util::GetStringUTF16(IDS_APP_ACCNAME_CLOSE)); 74 AddChildView(close_button_); 75 maximize_button_ = new views::ImageButton(this); 76 maximize_button_->SetImage(views::CustomButton::STATE_NORMAL, 77 rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE).ToImageSkia()); 78 maximize_button_->SetImage(views::CustomButton::STATE_HOVERED, 79 rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_H).ToImageSkia()); 80 maximize_button_->SetImage(views::CustomButton::STATE_PRESSED, 81 rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_P).ToImageSkia()); 82 maximize_button_->SetImage(views::CustomButton::STATE_DISABLED, 83 rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_D).ToImageSkia()); 84 maximize_button_->SetAccessibleName( 85 l10n_util::GetStringUTF16(IDS_APP_ACCNAME_MAXIMIZE)); 86 AddChildView(maximize_button_); 87 restore_button_ = new views::ImageButton(this); 88 restore_button_->SetImage(views::CustomButton::STATE_NORMAL, 89 rb.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE).ToImageSkia()); 90 restore_button_->SetImage(views::CustomButton::STATE_HOVERED, 91 rb.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE_H).ToImageSkia()); 92 restore_button_->SetImage(views::CustomButton::STATE_PRESSED, 93 rb.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE_P).ToImageSkia()); 94 restore_button_->SetAccessibleName( 95 l10n_util::GetStringUTF16(IDS_APP_ACCNAME_RESTORE)); 96 AddChildView(restore_button_); 97 minimize_button_ = new views::ImageButton(this); 98 minimize_button_->SetImage(views::CustomButton::STATE_NORMAL, 99 rb.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE).ToImageSkia()); 100 minimize_button_->SetImage(views::CustomButton::STATE_HOVERED, 101 rb.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE_H).ToImageSkia()); 102 minimize_button_->SetImage(views::CustomButton::STATE_PRESSED, 103 rb.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE_P).ToImageSkia()); 104 minimize_button_->SetAccessibleName( 105 l10n_util::GetStringUTF16(IDS_APP_ACCNAME_MINIMIZE)); 106 AddChildView(minimize_button_); 107 } 108 109 #if defined(USE_AURA) 110 aura::Window* window = frame->GetNativeWindow(); 111 // Some Aura implementations (Ash) allow resize handles outside the window. 112 if (resize_outside_bounds_size > 0) { 113 gfx::Insets mouse_insets = gfx::Insets(-resize_outside_bounds_size, 114 -resize_outside_bounds_size, 115 -resize_outside_bounds_size, 116 -resize_outside_bounds_size); 117 gfx::Insets touch_insets = 118 mouse_insets.Scale(resize_outside_scale_for_touch); 119 // Ensure we get resize cursors for a few pixels outside our bounds. 120 window->SetHitTestBoundsOverrideOuter(mouse_insets, touch_insets); 121 } 122 // Ensure we get resize cursors just inside our bounds as well. 123 // TODO(jeremya): do we need to update these when in fullscreen/maximized? 124 window->set_hit_test_bounds_override_inner( 125 gfx::Insets(resize_inside_bounds_size_, resize_inside_bounds_size_, 126 resize_inside_bounds_size_, resize_inside_bounds_size_)); 127 #endif 128 } 129 130 // views::NonClientFrameView implementation. 131 132 gfx::Rect ShellWindowFrameView::GetBoundsForClientView() const { 133 if (window_->IsFrameless() || frame_->IsFullscreen()) 134 return bounds(); 135 return gfx::Rect(0, kCaptionHeight, width(), 136 std::max(0, height() - kCaptionHeight)); 137 } 138 139 gfx::Rect ShellWindowFrameView::GetWindowBoundsForClientBounds( 140 const gfx::Rect& client_bounds) const { 141 if (window_->IsFrameless()) { 142 gfx::Rect window_bounds = client_bounds; 143 // Enforce minimum size (1, 1) in case that client_bounds is passed with 144 // empty size. This could occur when the frameless window is being 145 // initialized. 146 if (window_bounds.IsEmpty()) { 147 window_bounds.set_width(1); 148 window_bounds.set_height(1); 149 } 150 return window_bounds; 151 } 152 153 int closeButtonOffsetX = 154 (kCaptionHeight - close_button_->height()) / 2; 155 int header_width = close_button_->width() + closeButtonOffsetX * 2; 156 return gfx::Rect(client_bounds.x(), 157 std::max(0, client_bounds.y() - kCaptionHeight), 158 std::max(header_width, client_bounds.width()), 159 client_bounds.height() + kCaptionHeight); 160 } 161 162 int ShellWindowFrameView::NonClientHitTest(const gfx::Point& point) { 163 if (frame_->IsFullscreen()) 164 return HTCLIENT; 165 166 gfx::Rect expanded_bounds = bounds(); 167 #if defined(USE_AURA) 168 // Some Aura implementations (Ash) optionally allow resize handles just 169 // outside the window bounds. 170 aura::Window* window = frame_->GetNativeWindow(); 171 if (aura::Env::GetInstance()->is_touch_down()) 172 expanded_bounds.Inset(window->hit_test_bounds_override_outer_touch()); 173 else 174 expanded_bounds.Inset(window->hit_test_bounds_override_outer_mouse()); 175 #endif 176 // Points outside the (possibly expanded) bounds can be discarded. 177 if (!expanded_bounds.Contains(point)) 178 return HTNOWHERE; 179 180 // Check the frame first, as we allow a small area overlapping the contents 181 // to be used for resize handles. 182 bool can_ever_resize = frame_->widget_delegate() ? 183 frame_->widget_delegate()->CanResize() : 184 false; 185 // Don't allow overlapping resize handles when the window is maximized or 186 // fullscreen, as it can't be resized in those states. 187 int resize_border = 188 (frame_->IsMaximized() || frame_->IsFullscreen()) ? 0 : 189 resize_inside_bounds_size_; 190 int frame_component = GetHTComponentForFrame(point, 191 resize_border, 192 resize_border, 193 resize_area_corner_size_, 194 resize_area_corner_size_, 195 can_ever_resize); 196 if (frame_component != HTNOWHERE) 197 return frame_component; 198 199 // Check for possible draggable region in the client area for the frameless 200 // window. 201 if (window_->IsFrameless()) { 202 SkRegion* draggable_region = window_->GetDraggableRegion(); 203 if (draggable_region && draggable_region->contains(point.x(), point.y())) 204 return HTCAPTION; 205 } 206 207 int client_component = frame_->client_view()->NonClientHitTest(point); 208 if (client_component != HTNOWHERE) 209 return client_component; 210 211 // Then see if the point is within any of the window controls. 212 if (close_button_ && close_button_->visible() && 213 close_button_->GetMirroredBounds().Contains(point)) { 214 return HTCLOSE; 215 } 216 if ((maximize_button_ && maximize_button_->visible() && 217 maximize_button_->GetMirroredBounds().Contains(point)) || 218 (restore_button_ && restore_button_->visible() && 219 restore_button_->GetMirroredBounds().Contains(point))) { 220 return HTMAXBUTTON; 221 } 222 if (minimize_button_ && minimize_button_->visible() && 223 minimize_button_->GetMirroredBounds().Contains(point)) { 224 return HTMINBUTTON; 225 } 226 227 // Caption is a safe default. 228 return HTCAPTION; 229 } 230 231 void ShellWindowFrameView::GetWindowMask(const gfx::Size& size, 232 gfx::Path* window_mask) { 233 // We got nothing to say about no window mask. 234 } 235 236 // views::View implementation. 237 238 gfx::Size ShellWindowFrameView::GetPreferredSize() { 239 gfx::Size pref = frame_->client_view()->GetPreferredSize(); 240 gfx::Rect bounds(0, 0, pref.width(), pref.height()); 241 return frame_->non_client_view()->GetWindowBoundsForClientBounds( 242 bounds).size(); 243 } 244 245 void ShellWindowFrameView::Layout() { 246 if (window_->IsFrameless()) 247 return; 248 gfx::Size close_size = close_button_->GetPreferredSize(); 249 const int kButtonOffsetY = 0; 250 const int kButtonSpacing = 1; 251 const int kRightMargin = 3; 252 253 close_button_->SetBounds( 254 width() - kRightMargin - close_size.width(), 255 kButtonOffsetY, 256 close_size.width(), 257 close_size.height()); 258 259 bool can_ever_resize = frame_->widget_delegate() ? 260 frame_->widget_delegate()->CanResize() : 261 false; 262 maximize_button_->SetEnabled(can_ever_resize); 263 gfx::Size maximize_size = maximize_button_->GetPreferredSize(); 264 maximize_button_->SetBounds( 265 close_button_->x() - kButtonSpacing - maximize_size.width(), 266 kButtonOffsetY, 267 maximize_size.width(), 268 maximize_size.height()); 269 gfx::Size restore_size = restore_button_->GetPreferredSize(); 270 restore_button_->SetBounds( 271 close_button_->x() - kButtonSpacing - restore_size.width(), 272 kButtonOffsetY, 273 restore_size.width(), 274 restore_size.height()); 275 276 bool maximized = frame_->IsMaximized(); 277 maximize_button_->SetVisible(!maximized); 278 restore_button_->SetVisible(maximized); 279 if (maximized) 280 maximize_button_->SetState(views::CustomButton::STATE_NORMAL); 281 else 282 restore_button_->SetState(views::CustomButton::STATE_NORMAL); 283 284 gfx::Size minimize_size = minimize_button_->GetPreferredSize(); 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 ShellWindowFrameView::OnPaint(gfx::Canvas* canvas) { 293 if (window_->IsFrameless()) 294 return; 295 296 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 297 if (ShouldPaintAsActive()) { 298 close_button_->SetImage(views::CustomButton::STATE_NORMAL, 299 rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE).ToImageSkia()); 300 } else { 301 close_button_->SetImage(views::CustomButton::STATE_NORMAL, 302 rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE_U).ToImageSkia()); 303 } 304 305 // TODO(jeremya): different look for inactive? 306 SkPaint paint; 307 paint.setAntiAlias(false); 308 paint.setStyle(SkPaint::kFill_Style); 309 paint.setColor(SK_ColorWHITE); 310 gfx::Path path; 311 const int radius = frame_->IsMaximized() ? 0 : 1; 312 path.moveTo(0, radius); 313 path.lineTo(radius, 0); 314 path.lineTo(width() - radius - 1, 0); 315 path.lineTo(width(), radius + 1); 316 path.lineTo(width(), kCaptionHeight); 317 path.lineTo(0, kCaptionHeight); 318 path.close(); 319 canvas->DrawPath(path, paint); 320 } 321 322 const char* ShellWindowFrameView::GetClassName() const { 323 return kViewClassName; 324 } 325 326 gfx::Size ShellWindowFrameView::GetMinimumSize() { 327 gfx::Size min_size = frame_->client_view()->GetMinimumSize(); 328 if (window_->IsFrameless()) 329 return min_size; 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 = 337 (kCaptionHeight - close_button_->height()) / 2; 338 int header_width = close_button_->width() + closeButtonOffsetX * 2; 339 if (header_width > min_size.width()) 340 min_size.set_width(header_width); 341 return min_size; 342 } 343 344 gfx::Size ShellWindowFrameView::GetMaximumSize() { 345 gfx::Size max_size = frame_->client_view()->GetMaximumSize(); 346 347 // Add to the client maximum size the height of any title bar and borders. 348 gfx::Size client_size = GetBoundsForClientView().size(); 349 if (max_size.width()) 350 max_size.Enlarge(width() - client_size.width(), 0); 351 if (max_size.height()) 352 max_size.Enlarge(0, height() - client_size.height()); 353 354 return max_size; 355 } 356 357 // views::ButtonListener implementation. 358 359 void ShellWindowFrameView::ButtonPressed(views::Button* sender, 360 const ui::Event& event) { 361 DCHECK(!window_->IsFrameless()); 362 if (sender == close_button_) 363 frame_->Close(); 364 else if (sender == maximize_button_) 365 frame_->Maximize(); 366 else if (sender == restore_button_) 367 frame_->Restore(); 368 else if (sender == minimize_button_) 369 frame_->Minimize(); 370 } 371 372 } // namespace apps 373