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 "chrome/browser/ui/views/frame/browser_non_client_frame_view_ash.h" 6 7#include "ash/ash_switches.h" 8#include "ash/wm/caption_buttons/frame_caption_button_container_view.h" 9#include "ash/wm/frame_border_hit_test_controller.h" 10#include "ash/wm/header_painter.h" 11#include "chrome/browser/themes/theme_properties.h" 12#include "chrome/browser/ui/browser.h" 13#include "chrome/browser/ui/views/avatar_label.h" 14#include "chrome/browser/ui/views/avatar_menu_button.h" 15#include "chrome/browser/ui/views/frame/browser_frame.h" 16#include "chrome/browser/ui/views/frame/browser_view.h" 17#include "chrome/browser/ui/views/frame/immersive_mode_controller.h" 18#include "chrome/browser/ui/views/tab_icon_view.h" 19#include "chrome/browser/ui/views/tabs/tab_strip.h" 20#include "content/public/browser/web_contents.h" 21#include "grit/ash_resources.h" 22#include "grit/theme_resources.h" 23#include "ui/aura/client/aura_constants.h" 24#include "ui/aura/window.h" 25#include "ui/base/accessibility/accessible_view_state.h" 26#include "ui/base/hit_test.h" 27#include "ui/base/l10n/l10n_util.h" 28#include "ui/base/layout.h" 29#include "ui/base/resource/resource_bundle.h" 30#include "ui/base/theme_provider.h" 31#include "ui/compositor/layer_animator.h" 32#include "ui/gfx/canvas.h" 33#include "ui/gfx/image/image_skia.h" 34#include "ui/gfx/rect_conversions.h" 35#include "ui/views/controls/label.h" 36#include "ui/views/layout/layout_constants.h" 37#include "ui/views/widget/widget.h" 38#include "ui/views/widget/widget_delegate.h" 39 40namespace { 41 42// The avatar ends 2 px above the bottom of the tabstrip (which, given the 43// way the tabstrip draws its bottom edge, will appear like a 1 px gap to the 44// user). 45const int kAvatarBottomSpacing = 2; 46// There are 2 px on each side of the avatar (between the frame border and 47// it on the left, and between it and the tabstrip on the right). 48const int kAvatarSideSpacing = 2; 49// Space between left edge of window and tabstrip. 50const int kTabstripLeftSpacing = 0; 51// Space between right edge of tabstrip and maximize button. 52const int kTabstripRightSpacing = 10; 53// Height of the shadow of the content area, at the top of the toolbar. 54const int kContentShadowHeight = 1; 55// Space between top of window and top of tabstrip for tall headers, such as 56// for restored windows, apps, etc. 57const int kTabstripTopSpacingTall = 7; 58// Space between top of window and top of tabstrip for short headers, such as 59// for maximized windows, pop-ups, etc. 60const int kTabstripTopSpacingShort = 0; 61// Height of the shadow in the tab image, used to ensure clicks in the shadow 62// area still drag restored windows. This keeps the clickable area large enough 63// to hit easily. 64const int kTabShadowHeight = 4; 65 66} // namespace 67 68/////////////////////////////////////////////////////////////////////////////// 69// BrowserNonClientFrameViewAsh, public: 70 71// static 72const char BrowserNonClientFrameViewAsh::kViewClassName[] = 73 "BrowserNonClientFrameViewAsh"; 74 75BrowserNonClientFrameViewAsh::BrowserNonClientFrameViewAsh( 76 BrowserFrame* frame, BrowserView* browser_view) 77 : BrowserNonClientFrameView(frame, browser_view), 78 caption_button_container_(NULL), 79 window_icon_(NULL), 80 header_painter_(new ash::HeaderPainter), 81 frame_border_hit_test_controller_( 82 new ash::FrameBorderHitTestController(frame)) { 83} 84 85BrowserNonClientFrameViewAsh::~BrowserNonClientFrameViewAsh() { 86} 87 88void BrowserNonClientFrameViewAsh::Init() { 89 caption_button_container_ = new ash::FrameCaptionButtonContainerView(frame(), 90 ash::FrameCaptionButtonContainerView::MINIMIZE_ALLOWED); 91 AddChildView(caption_button_container_); 92 93 // Initializing the TabIconView is expensive, so only do it if we need to. 94 if (browser_view()->ShouldShowWindowIcon()) { 95 window_icon_ = new TabIconView(this); 96 window_icon_->set_is_light(true); 97 AddChildView(window_icon_); 98 window_icon_->Update(); 99 } 100 101 // Create incognito icon if necessary. 102 UpdateAvatarInfo(); 103 104 // Frame painter handles layout. 105 header_painter_->Init(frame(), this, window_icon_, caption_button_container_); 106} 107 108/////////////////////////////////////////////////////////////////////////////// 109// BrowserNonClientFrameView overrides: 110 111gfx::Rect BrowserNonClientFrameViewAsh::GetBoundsForTabStrip( 112 views::View* tabstrip) const { 113 if (!tabstrip) 114 return gfx::Rect(); 115 116 // When the tab strip is painted in the immersive fullscreen light bar style, 117 // the caption buttons and the avatar button are not visible. However, their 118 // bounds are still used to compute the tab strip bounds so that the tabs have 119 // the same horizontal position when the tab strip is painted in the immersive 120 // light bar style as when the top-of-window views are revealed. 121 int left_inset = GetTabStripLeftInset(); 122 int right_inset = GetTabStripRightInset(); 123 return gfx::Rect(left_inset, 124 GetTopInset(), 125 std::max(0, width() - left_inset - right_inset), 126 tabstrip->GetPreferredSize().height()); 127} 128 129int BrowserNonClientFrameViewAsh::GetTopInset() const { 130 if (!ShouldPaint() || UseImmersiveLightbarHeaderStyle()) 131 return 0; 132 133 if (browser_view()->IsTabStripVisible()) { 134 if (UseShortHeader()) 135 return kTabstripTopSpacingShort; 136 else 137 return kTabstripTopSpacingTall; 138 } 139 140 int caption_buttons_bottom = caption_button_container_->bounds().bottom(); 141 142 // The toolbar partially overlaps the caption buttons. 143 if (browser_view()->IsToolbarVisible()) 144 return caption_buttons_bottom - kContentShadowHeight; 145 146 return caption_buttons_bottom + kClientEdgeThickness; 147} 148 149int BrowserNonClientFrameViewAsh::GetThemeBackgroundXInset() const { 150 return header_painter_->GetThemeBackgroundXInset(); 151} 152 153void BrowserNonClientFrameViewAsh::UpdateThrobber(bool running) { 154 if (window_icon_) 155 window_icon_->Update(); 156} 157 158/////////////////////////////////////////////////////////////////////////////// 159// views::NonClientFrameView overrides: 160 161gfx::Rect BrowserNonClientFrameViewAsh::GetBoundsForClientView() const { 162 // The ClientView must be flush with the top edge of the widget so that the 163 // web contents can take up the entire screen in immersive fullscreen (with 164 // or without the top-of-window views revealed). When in immersive fullscreen 165 // and the top-of-window views are revealed, the TopContainerView paints the 166 // window header by redirecting paints from its background to 167 // BrowserNonClientFrameViewAsh. 168 return ash::HeaderPainter::GetBoundsForClientView(0, bounds()); 169} 170 171gfx::Rect BrowserNonClientFrameViewAsh::GetWindowBoundsForClientBounds( 172 const gfx::Rect& client_bounds) const { 173 return ash::HeaderPainter::GetWindowBoundsForClientBounds(0, client_bounds); 174} 175 176int BrowserNonClientFrameViewAsh::NonClientHitTest(const gfx::Point& point) { 177 int hit_test = ash::FrameBorderHitTestController::NonClientHitTest(this, 178 header_painter_.get(), point); 179 180 // See if the point is actually within the avatar menu button or within 181 // the avatar label. 182 if (hit_test == HTCAPTION && ((avatar_button() && 183 avatar_button()->GetMirroredBounds().Contains(point)) || 184 (avatar_label() && avatar_label()->GetMirroredBounds().Contains(point)))) 185 return HTCLIENT; 186 187 // When the window is restored we want a large click target above the tabs 188 // to drag the window, so redirect clicks in the tab's shadow to caption. 189 if (hit_test == HTCLIENT && 190 !(frame()->IsMaximized() || frame()->IsFullscreen())) { 191 // Convert point to client coordinates. 192 gfx::Point client_point(point); 193 View::ConvertPointToTarget(this, frame()->client_view(), &client_point); 194 // Report hits in shadow at top of tabstrip as caption. 195 gfx::Rect tabstrip_bounds(browser_view()->tabstrip()->bounds()); 196 if (client_point.y() < tabstrip_bounds.y() + kTabShadowHeight) 197 hit_test = HTCAPTION; 198 } 199 return hit_test; 200} 201 202void BrowserNonClientFrameViewAsh::GetWindowMask(const gfx::Size& size, 203 gfx::Path* window_mask) { 204 // Aura does not use window masks. 205} 206 207void BrowserNonClientFrameViewAsh::ResetWindowControls() { 208 // Hide the caption buttons in immersive fullscreen when the tab light bar 209 // is visible because it's confusing when the user hovers or clicks in the 210 // top-right of the screen and hits one. 211 bool button_visibility = !UseImmersiveLightbarHeaderStyle(); 212 caption_button_container_->SetVisible(button_visibility); 213 214 caption_button_container_->ResetWindowControls(); 215} 216 217void BrowserNonClientFrameViewAsh::UpdateWindowIcon() { 218 if (window_icon_) 219 window_icon_->SchedulePaint(); 220} 221 222void BrowserNonClientFrameViewAsh::UpdateWindowTitle() { 223 if (!frame()->IsFullscreen()) 224 header_painter_->SchedulePaintForTitle(BrowserFrame::GetTitleFont()); 225} 226 227/////////////////////////////////////////////////////////////////////////////// 228// views::View overrides: 229 230void BrowserNonClientFrameViewAsh::OnPaint(gfx::Canvas* canvas) { 231 if (!ShouldPaint()) 232 return; 233 234 if (UseImmersiveLightbarHeaderStyle()) { 235 PaintImmersiveLightbarStyleHeader(canvas); 236 return; 237 } 238 239 // The primary header image changes based on window activation state and 240 // theme, so we look it up for each paint. 241 int theme_frame_image_id = GetThemeFrameImageId(); 242 int theme_frame_overlay_image_id = GetThemeFrameOverlayImageId(); 243 244 ui::ThemeProvider* theme_provider = GetThemeProvider(); 245 if (!theme_provider->HasCustomImage(theme_frame_image_id) && 246 (theme_frame_overlay_image_id == 0 || 247 !theme_provider->HasCustomImage(theme_frame_overlay_image_id))) { 248 if (frame()->IsMaximized() || frame()->IsFullscreen()) 249 theme_frame_image_id = IDR_AURA_WINDOW_HEADER_BASE_MINIMAL; 250 } 251 header_painter_->PaintHeader( 252 canvas, 253 ShouldPaintAsActive() ? 254 ash::HeaderPainter::ACTIVE : ash::HeaderPainter::INACTIVE, 255 theme_frame_image_id, 256 theme_frame_overlay_image_id); 257 if (browser_view()->ShouldShowWindowTitle()) 258 header_painter_->PaintTitleBar(canvas, BrowserFrame::GetTitleFont()); 259 if (browser_view()->IsToolbarVisible()) 260 PaintToolbarBackground(canvas); 261 else 262 PaintContentEdge(canvas); 263} 264 265void BrowserNonClientFrameViewAsh::Layout() { 266 int header_height = 0; 267 if (browser_view()->IsTabStripVisible()) { 268 header_height = GetTopInset() + 269 browser_view()->tabstrip()->GetPreferredSize().height(); 270 } else if (browser_view()->IsToolbarVisible()) { 271 // Set the header's height so that it overlaps with the toolbar because the 272 // top few pixels of the toolbar are not opaque. 273 header_height = GetTopInset() + kFrameShadowThickness * 2; 274 } else { 275 header_height = GetTopInset(); 276 } 277 header_painter_->set_header_height(header_height); 278 header_painter_->LayoutHeader(UseShortHeader()); 279 if (avatar_button()) 280 LayoutAvatar(); 281 BrowserNonClientFrameView::Layout(); 282} 283 284const char* BrowserNonClientFrameViewAsh::GetClassName() const { 285 return kViewClassName; 286} 287 288bool BrowserNonClientFrameViewAsh::HitTestRect(const gfx::Rect& rect) const { 289 if (!views::View::HitTestRect(rect)) { 290 // |rect| is outside BrowserNonClientFrameViewAsh's bounds. 291 return false; 292 } 293 294 TabStrip* tabstrip = browser_view()->tabstrip(); 295 if (tabstrip && browser_view()->IsTabStripVisible()) { 296 // Claim |rect| only if it is above the bottom of the tabstrip in a non-tab 297 // portion. 298 gfx::RectF rect_in_tabstrip_coords_f(rect); 299 View::ConvertRectToTarget(this, tabstrip, &rect_in_tabstrip_coords_f); 300 gfx::Rect rect_in_tabstrip_coords = gfx::ToEnclosingRect( 301 rect_in_tabstrip_coords_f); 302 303 if (rect_in_tabstrip_coords.y() > tabstrip->height()) 304 return false; 305 306 return !tabstrip->HitTestRect(rect_in_tabstrip_coords) || 307 tabstrip->IsRectInWindowCaption(rect_in_tabstrip_coords); 308 } 309 310 // Claim |rect| if it is above the top of the topmost view in the client area. 311 return rect.y() < GetTopInset(); 312} 313 314void BrowserNonClientFrameViewAsh::GetAccessibleState( 315 ui::AccessibleViewState* state) { 316 state->role = ui::AccessibilityTypes::ROLE_TITLEBAR; 317} 318 319gfx::Size BrowserNonClientFrameViewAsh::GetMinimumSize() { 320 gfx::Size min_client_view_size(frame()->client_view()->GetMinimumSize()); 321 int min_width = std::max(header_painter_->GetMinimumHeaderWidth(), 322 min_client_view_size.width()); 323 if (browser_view()->IsTabStripVisible()) { 324 // Ensure that the minimum width is enough to hold a minimum width tab strip 325 // at its usual insets. 326 int min_tabstrip_width = 327 browser_view()->tabstrip()->GetMinimumSize().width(); 328 min_width = std::max(min_width, 329 min_tabstrip_width + GetTabStripLeftInset() + GetTabStripRightInset()); 330 } 331 return gfx::Size(min_width, min_client_view_size.height()); 332} 333 334void BrowserNonClientFrameViewAsh::OnThemeChanged() { 335 BrowserNonClientFrameView::OnThemeChanged(); 336 header_painter_->OnThemeChanged(); 337} 338 339/////////////////////////////////////////////////////////////////////////////// 340// chrome::TabIconViewModel overrides: 341 342bool BrowserNonClientFrameViewAsh::ShouldTabIconViewAnimate() const { 343 // This function is queried during the creation of the window as the 344 // TabIconView we host is initialized, so we need to NULL check the selected 345 // WebContents because in this condition there is not yet a selected tab. 346 content::WebContents* current_tab = browser_view()->GetActiveWebContents(); 347 return current_tab ? current_tab->IsLoading() : false; 348} 349 350gfx::ImageSkia BrowserNonClientFrameViewAsh::GetFaviconForTabIconView() { 351 views::WidgetDelegate* delegate = frame()->widget_delegate(); 352 if (!delegate) 353 return gfx::ImageSkia(); 354 return delegate->GetWindowIcon(); 355} 356 357/////////////////////////////////////////////////////////////////////////////// 358// BrowserNonClientFrameViewAsh, private: 359 360int BrowserNonClientFrameViewAsh::GetTabStripLeftInset() const { 361 return avatar_button() ? kAvatarSideSpacing + 362 browser_view()->GetOTRAvatarIcon().width() + kAvatarSideSpacing : 363 kTabstripLeftSpacing; 364} 365 366int BrowserNonClientFrameViewAsh::GetTabStripRightInset() const { 367 return header_painter_->GetRightInset() + kTabstripRightSpacing; 368} 369 370bool BrowserNonClientFrameViewAsh::UseShortHeader() const { 371 // Restored tabbed browser windows use the tall header. All other windows use 372 // the short header. 373 return frame()->IsMaximized() || 374 frame()->IsFullscreen() || 375 !browser_view()->IsBrowserTypeNormal(); 376} 377 378bool BrowserNonClientFrameViewAsh::UseImmersiveLightbarHeaderStyle() const { 379 ImmersiveModeController* immersive_controller = 380 browser_view()->immersive_mode_controller(); 381 return immersive_controller->IsEnabled() && 382 !immersive_controller->IsRevealed() && 383 browser_view()->IsTabStripVisible(); 384} 385 386void BrowserNonClientFrameViewAsh::LayoutAvatar() { 387 DCHECK(avatar_button()); 388 DCHECK(browser_view()->IsTabStripVisible()); 389 gfx::ImageSkia incognito_icon = browser_view()->GetOTRAvatarIcon(); 390 391 int avatar_bottom = GetTopInset() + 392 browser_view()->GetTabStripHeight() - kAvatarBottomSpacing; 393 int avatar_restored_y = avatar_bottom - incognito_icon.height(); 394 int avatar_y = (frame()->IsMaximized() || frame()->IsFullscreen()) ? 395 GetTopInset() + kContentShadowHeight : avatar_restored_y; 396 397 // Hide the incognito icon in immersive fullscreen when the tab light bar is 398 // visible because the header is too short for the icognito icon to be 399 // recognizable. 400 bool avatar_visible = !UseImmersiveLightbarHeaderStyle(); 401 int avatar_height = avatar_visible ? avatar_bottom - avatar_y : 0; 402 403 gfx::Rect avatar_bounds(kAvatarSideSpacing, 404 avatar_y, 405 incognito_icon.width(), 406 avatar_height); 407 avatar_button()->SetBoundsRect(avatar_bounds); 408 avatar_button()->SetVisible(avatar_visible); 409} 410 411bool BrowserNonClientFrameViewAsh::ShouldPaint() const { 412 if (!frame()->IsFullscreen()) 413 return true; 414 415 // We need to paint when in immersive fullscreen and either: 416 // - The top-of-window views are revealed. 417 // - The lightbar style tabstrip is visible. 418 ImmersiveModeController* immersive_mode_controller = 419 browser_view()->immersive_mode_controller(); 420 return immersive_mode_controller->IsEnabled() && 421 (immersive_mode_controller->IsRevealed() || 422 UseImmersiveLightbarHeaderStyle()); 423} 424 425void BrowserNonClientFrameViewAsh::PaintImmersiveLightbarStyleHeader( 426 gfx::Canvas* canvas) { 427 // The light bar header is not themed because theming it does not look good. 428 gfx::ImageSkia* frame_image = GetThemeProvider()->GetImageSkiaNamed( 429 IDR_AURA_WINDOW_HEADER_BASE_MINIMAL); 430 canvas->TileImageInt(*frame_image, 0, 0, width(), frame_image->height()); 431} 432 433void BrowserNonClientFrameViewAsh::PaintToolbarBackground(gfx::Canvas* canvas) { 434 gfx::Rect toolbar_bounds(browser_view()->GetToolbarBounds()); 435 if (toolbar_bounds.IsEmpty()) 436 return; 437 gfx::Point toolbar_origin(toolbar_bounds.origin()); 438 View::ConvertPointToTarget(browser_view(), this, &toolbar_origin); 439 toolbar_bounds.set_origin(toolbar_origin); 440 441 int x = toolbar_bounds.x(); 442 int w = toolbar_bounds.width(); 443 int y = toolbar_bounds.y(); 444 int h = toolbar_bounds.height(); 445 446 // Gross hack: We split the toolbar images into two pieces, since sometimes 447 // (popup mode) the toolbar isn't tall enough to show the whole image. The 448 // split happens between the top shadow section and the bottom gradient 449 // section so that we never break the gradient. 450 // NOTE(pkotwicz): If the computation for |bottom_y| is changed, Layout() must 451 // be changed as well. 452 int split_point = kFrameShadowThickness * 2; 453 int bottom_y = y + split_point; 454 ui::ThemeProvider* tp = GetThemeProvider(); 455 int bottom_edge_height = h - split_point; 456 457 canvas->FillRect(gfx::Rect(x, bottom_y, w, bottom_edge_height), 458 tp->GetColor(ThemeProperties::COLOR_TOOLBAR)); 459 460 // Paint the main toolbar image. Since this image is also used to draw the 461 // tab background, we must use the tab strip offset to compute the image 462 // source y position. If you have to debug this code use an image editor 463 // to paint a diagonal line through the toolbar image and ensure it lines up 464 // across the tab and toolbar. 465 gfx::ImageSkia* theme_toolbar = tp->GetImageSkiaNamed(IDR_THEME_TOOLBAR); 466 canvas->TileImageInt( 467 *theme_toolbar, 468 x + GetThemeBackgroundXInset(), 469 bottom_y - GetTopInset(), 470 x, bottom_y, 471 w, theme_toolbar->height()); 472 473 // The content area line has a shadow that extends a couple of pixels above 474 // the toolbar bounds. 475 const int kContentShadowHeight = 2; 476 gfx::ImageSkia* toolbar_top = tp->GetImageSkiaNamed(IDR_TOOLBAR_SHADE_TOP); 477 canvas->TileImageInt(*toolbar_top, 478 0, 0, 479 x, y - kContentShadowHeight, 480 w, split_point + kContentShadowHeight + 1); 481 482 // Draw the "lightening" shade line around the edges of the toolbar. 483 gfx::ImageSkia* toolbar_left = tp->GetImageSkiaNamed(IDR_TOOLBAR_SHADE_LEFT); 484 canvas->TileImageInt(*toolbar_left, 485 0, 0, 486 x + kClientEdgeThickness, 487 y + kClientEdgeThickness + kContentShadowHeight, 488 toolbar_left->width(), theme_toolbar->height()); 489 gfx::ImageSkia* toolbar_right = 490 tp->GetImageSkiaNamed(IDR_TOOLBAR_SHADE_RIGHT); 491 canvas->TileImageInt(*toolbar_right, 492 0, 0, 493 w - toolbar_right->width() - 2 * kClientEdgeThickness, 494 y + kClientEdgeThickness + kContentShadowHeight, 495 toolbar_right->width(), theme_toolbar->height()); 496 497 // Draw the content/toolbar separator. 498 canvas->FillRect( 499 gfx::Rect(x + kClientEdgeThickness, 500 toolbar_bounds.bottom() - kClientEdgeThickness, 501 w - (2 * kClientEdgeThickness), 502 kClientEdgeThickness), 503 ThemeProperties::GetDefaultColor( 504 ThemeProperties::COLOR_TOOLBAR_SEPARATOR)); 505} 506 507void BrowserNonClientFrameViewAsh::PaintContentEdge(gfx::Canvas* canvas) { 508 canvas->FillRect(gfx::Rect(0, caption_button_container_->bounds().bottom(), 509 width(), kClientEdgeThickness), 510 ThemeProperties::GetDefaultColor( 511 ThemeProperties::COLOR_TOOLBAR_SEPARATOR)); 512} 513 514int BrowserNonClientFrameViewAsh::GetThemeFrameImageId() const { 515 bool is_incognito = !browser_view()->IsRegularOrGuestSession(); 516 if (browser_view()->IsBrowserTypeNormal()) { 517 // Use the standard resource ids to allow users to theme the frames. 518 if (ShouldPaintAsActive()) { 519 return is_incognito ? 520 IDR_THEME_FRAME_INCOGNITO : IDR_THEME_FRAME; 521 } 522 return is_incognito ? 523 IDR_THEME_FRAME_INCOGNITO_INACTIVE : IDR_THEME_FRAME_INACTIVE; 524 } 525 // Never theme app and popup windows. 526 if (ShouldPaintAsActive()) { 527 return is_incognito ? 528 IDR_AURA_WINDOW_HEADER_BASE_INCOGNITO_ACTIVE : 529 IDR_AURA_WINDOW_HEADER_BASE_ACTIVE; 530 } 531 return is_incognito ? 532 IDR_AURA_WINDOW_HEADER_BASE_INCOGNITO_INACTIVE : 533 IDR_AURA_WINDOW_HEADER_BASE_INACTIVE; 534} 535 536int BrowserNonClientFrameViewAsh::GetThemeFrameOverlayImageId() const { 537 ui::ThemeProvider* tp = GetThemeProvider(); 538 if (tp->HasCustomImage(IDR_THEME_FRAME_OVERLAY) && 539 browser_view()->IsBrowserTypeNormal() && 540 !browser_view()->IsOffTheRecord()) { 541 return ShouldPaintAsActive() ? 542 IDR_THEME_FRAME_OVERLAY : IDR_THEME_FRAME_OVERLAY_INACTIVE; 543 } 544 return 0; 545} 546