| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "apps/ui/views/app_window_frame_view.h" |
| |
| #include "base/strings/utf_string_conversions.h" |
| #include "extensions/browser/app_window/native_app_window.h" |
| #include "extensions/common/draggable_region.h" |
| #include "grit/theme_resources.h" |
| #include "third_party/skia/include/core/SkPaint.h" |
| #include "third_party/skia/include/core/SkRegion.h" |
| #include "ui/base/hit_test.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/color_utils.h" |
| #include "ui/gfx/image/image.h" |
| #include "ui/gfx/path.h" |
| #include "ui/strings/grit/ui_strings.h" // Accessibility names |
| #include "ui/views/controls/button/image_button.h" |
| #include "ui/views/layout/grid_layout.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/widget/widget_delegate.h" |
| |
| namespace { |
| |
| const int kDefaultResizeInsideBoundsSize = 5; |
| const int kDefaultResizeAreaCornerSize = 16; |
| const int kCaptionHeight = 25; |
| |
| } // namespace |
| |
| namespace apps { |
| |
| const char AppWindowFrameView::kViewClassName[] = |
| "browser/ui/views/extensions/AppWindowFrameView"; |
| |
| AppWindowFrameView::AppWindowFrameView(views::Widget* widget, |
| extensions::NativeAppWindow* window, |
| bool draw_frame, |
| const SkColor& active_frame_color, |
| const SkColor& inactive_frame_color) |
| : widget_(widget), |
| window_(window), |
| draw_frame_(draw_frame), |
| active_frame_color_(active_frame_color), |
| inactive_frame_color_(inactive_frame_color), |
| close_button_(NULL), |
| maximize_button_(NULL), |
| restore_button_(NULL), |
| minimize_button_(NULL), |
| resize_inside_bounds_size_(kDefaultResizeInsideBoundsSize), |
| resize_outside_bounds_size_(0), |
| resize_area_corner_size_(kDefaultResizeAreaCornerSize) { |
| } |
| |
| AppWindowFrameView::~AppWindowFrameView() {} |
| |
| void AppWindowFrameView::Init() { |
| if (draw_frame_) { |
| ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); |
| close_button_ = new views::ImageButton(this); |
| close_button_->SetImage( |
| views::CustomButton::STATE_NORMAL, |
| rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE).ToImageSkia()); |
| close_button_->SetImage( |
| views::CustomButton::STATE_HOVERED, |
| rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE_H).ToImageSkia()); |
| close_button_->SetImage( |
| views::CustomButton::STATE_PRESSED, |
| rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE_P).ToImageSkia()); |
| close_button_->SetAccessibleName( |
| l10n_util::GetStringUTF16(IDS_APP_ACCNAME_CLOSE)); |
| AddChildView(close_button_); |
| // STATE_NORMAL images are set in SetButtonImagesForFrame, not here. |
| maximize_button_ = new views::ImageButton(this); |
| maximize_button_->SetImage( |
| views::CustomButton::STATE_HOVERED, |
| rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_H).ToImageSkia()); |
| maximize_button_->SetImage( |
| views::CustomButton::STATE_PRESSED, |
| rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_P).ToImageSkia()); |
| maximize_button_->SetImage( |
| views::CustomButton::STATE_DISABLED, |
| rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_D).ToImageSkia()); |
| maximize_button_->SetAccessibleName( |
| l10n_util::GetStringUTF16(IDS_APP_ACCNAME_MAXIMIZE)); |
| AddChildView(maximize_button_); |
| restore_button_ = new views::ImageButton(this); |
| restore_button_->SetImage( |
| views::CustomButton::STATE_HOVERED, |
| rb.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE_H).ToImageSkia()); |
| restore_button_->SetImage( |
| views::CustomButton::STATE_PRESSED, |
| rb.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE_P).ToImageSkia()); |
| restore_button_->SetAccessibleName( |
| l10n_util::GetStringUTF16(IDS_APP_ACCNAME_RESTORE)); |
| AddChildView(restore_button_); |
| minimize_button_ = new views::ImageButton(this); |
| minimize_button_->SetImage( |
| views::CustomButton::STATE_HOVERED, |
| rb.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE_H).ToImageSkia()); |
| minimize_button_->SetImage( |
| views::CustomButton::STATE_PRESSED, |
| rb.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE_P).ToImageSkia()); |
| minimize_button_->SetAccessibleName( |
| l10n_util::GetStringUTF16(IDS_APP_ACCNAME_MINIMIZE)); |
| AddChildView(minimize_button_); |
| |
| SetButtonImagesForFrame(); |
| } |
| } |
| |
| void AppWindowFrameView::SetResizeSizes(int resize_inside_bounds_size, |
| int resize_outside_bounds_size, |
| int resize_area_corner_size) { |
| resize_inside_bounds_size_ = resize_inside_bounds_size; |
| resize_outside_bounds_size_ = resize_outside_bounds_size; |
| resize_area_corner_size_ = resize_area_corner_size; |
| } |
| |
| // views::NonClientFrameView implementation. |
| |
| gfx::Rect AppWindowFrameView::GetBoundsForClientView() const { |
| if (!draw_frame_ || widget_->IsFullscreen()) |
| return bounds(); |
| return gfx::Rect( |
| 0, kCaptionHeight, width(), std::max(0, height() - kCaptionHeight)); |
| } |
| |
| gfx::Rect AppWindowFrameView::GetWindowBoundsForClientBounds( |
| const gfx::Rect& client_bounds) const { |
| gfx::Rect window_bounds = client_bounds; |
| #if defined(OS_LINUX) && !defined(OS_CHROMEOS) |
| // Get the difference between the widget's client area bounds and window |
| // bounds, and grow |window_bounds| by that amount. |
| gfx::Insets native_frame_insets = |
| widget_->GetClientAreaBoundsInScreen().InsetsFrom( |
| widget_->GetWindowBoundsInScreen()); |
| window_bounds.Inset(native_frame_insets); |
| #endif |
| if (!draw_frame_) { |
| // Enforce minimum size (1, 1) in case that client_bounds is passed with |
| // empty size. This could occur when the frameless window is being |
| // initialized. |
| if (window_bounds.IsEmpty()) { |
| window_bounds.set_width(1); |
| window_bounds.set_height(1); |
| } |
| return window_bounds; |
| } |
| |
| int closeButtonOffsetX = (kCaptionHeight - close_button_->height()) / 2; |
| int header_width = close_button_->width() + closeButtonOffsetX * 2; |
| return gfx::Rect(window_bounds.x(), |
| window_bounds.y() - kCaptionHeight, |
| std::max(header_width, window_bounds.width()), |
| window_bounds.height() + kCaptionHeight); |
| } |
| |
| int AppWindowFrameView::NonClientHitTest(const gfx::Point& point) { |
| if (widget_->IsFullscreen()) |
| return HTCLIENT; |
| |
| gfx::Rect expanded_bounds = bounds(); |
| if (resize_outside_bounds_size_) { |
| expanded_bounds.Inset(gfx::Insets(-resize_outside_bounds_size_, |
| -resize_outside_bounds_size_, |
| -resize_outside_bounds_size_, |
| -resize_outside_bounds_size_)); |
| } |
| // Points outside the (possibly expanded) bounds can be discarded. |
| if (!expanded_bounds.Contains(point)) |
| return HTNOWHERE; |
| |
| // Check the frame first, as we allow a small area overlapping the contents |
| // to be used for resize handles. |
| bool can_ever_resize = widget_->widget_delegate() |
| ? widget_->widget_delegate()->CanResize() |
| : false; |
| // Don't allow overlapping resize handles when the window is maximized or |
| // fullscreen, as it can't be resized in those states. |
| int resize_border = (widget_->IsMaximized() || widget_->IsFullscreen()) |
| ? 0 |
| : resize_inside_bounds_size_; |
| int frame_component = GetHTComponentForFrame(point, |
| resize_border, |
| resize_border, |
| resize_area_corner_size_, |
| resize_area_corner_size_, |
| can_ever_resize); |
| if (frame_component != HTNOWHERE) |
| return frame_component; |
| |
| // Check for possible draggable region in the client area for the frameless |
| // window. |
| SkRegion* draggable_region = window_->GetDraggableRegion(); |
| if (draggable_region && draggable_region->contains(point.x(), point.y())) |
| return HTCAPTION; |
| |
| int client_component = widget_->client_view()->NonClientHitTest(point); |
| if (client_component != HTNOWHERE) |
| return client_component; |
| |
| // Then see if the point is within any of the window controls. |
| if (close_button_ && close_button_->visible() && |
| close_button_->GetMirroredBounds().Contains(point)) { |
| return HTCLOSE; |
| } |
| if ((maximize_button_ && maximize_button_->visible() && |
| maximize_button_->GetMirroredBounds().Contains(point)) || |
| (restore_button_ && restore_button_->visible() && |
| restore_button_->GetMirroredBounds().Contains(point))) { |
| return HTMAXBUTTON; |
| } |
| if (minimize_button_ && minimize_button_->visible() && |
| minimize_button_->GetMirroredBounds().Contains(point)) { |
| return HTMINBUTTON; |
| } |
| |
| // Caption is a safe default. |
| return HTCAPTION; |
| } |
| |
| void AppWindowFrameView::GetWindowMask(const gfx::Size& size, |
| gfx::Path* window_mask) { |
| // We got nothing to say about no window mask. |
| } |
| |
| void AppWindowFrameView::SizeConstraintsChanged() { |
| if (draw_frame_) { |
| maximize_button_->SetEnabled(widget_->widget_delegate() && |
| widget_->widget_delegate()->CanMaximize()); |
| } |
| } |
| |
| gfx::Size AppWindowFrameView::GetPreferredSize() const { |
| gfx::Size pref = widget_->client_view()->GetPreferredSize(); |
| gfx::Rect bounds(0, 0, pref.width(), pref.height()); |
| return widget_->non_client_view() |
| ->GetWindowBoundsForClientBounds(bounds) |
| .size(); |
| } |
| |
| void AppWindowFrameView::Layout() { |
| if (!draw_frame_) |
| return; |
| gfx::Size close_size = close_button_->GetPreferredSize(); |
| const int kButtonOffsetY = 0; |
| const int kButtonSpacing = 1; |
| const int kRightMargin = 3; |
| |
| close_button_->SetBounds(width() - kRightMargin - close_size.width(), |
| kButtonOffsetY, |
| close_size.width(), |
| close_size.height()); |
| |
| maximize_button_->SetEnabled(widget_->widget_delegate() && |
| widget_->widget_delegate()->CanMaximize()); |
| gfx::Size maximize_size = maximize_button_->GetPreferredSize(); |
| maximize_button_->SetBounds( |
| close_button_->x() - kButtonSpacing - maximize_size.width(), |
| kButtonOffsetY, |
| maximize_size.width(), |
| maximize_size.height()); |
| gfx::Size restore_size = restore_button_->GetPreferredSize(); |
| restore_button_->SetBounds( |
| close_button_->x() - kButtonSpacing - restore_size.width(), |
| kButtonOffsetY, |
| restore_size.width(), |
| restore_size.height()); |
| |
| bool maximized = widget_->IsMaximized(); |
| maximize_button_->SetVisible(!maximized); |
| restore_button_->SetVisible(maximized); |
| if (maximized) |
| maximize_button_->SetState(views::CustomButton::STATE_NORMAL); |
| else |
| restore_button_->SetState(views::CustomButton::STATE_NORMAL); |
| |
| gfx::Size minimize_size = minimize_button_->GetPreferredSize(); |
| minimize_button_->SetState(views::CustomButton::STATE_NORMAL); |
| minimize_button_->SetBounds( |
| maximize_button_->x() - kButtonSpacing - minimize_size.width(), |
| kButtonOffsetY, |
| minimize_size.width(), |
| minimize_size.height()); |
| } |
| |
| void AppWindowFrameView::OnPaint(gfx::Canvas* canvas) { |
| if (!draw_frame_) |
| return; |
| |
| ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); |
| if (ShouldPaintAsActive()) { |
| close_button_->SetImage( |
| views::CustomButton::STATE_NORMAL, |
| rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE).ToImageSkia()); |
| } else { |
| close_button_->SetImage( |
| views::CustomButton::STATE_NORMAL, |
| rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE_U).ToImageSkia()); |
| } |
| |
| SetButtonImagesForFrame(); |
| // TODO(benwells): different look for inactive by default. |
| SkPaint paint; |
| paint.setAntiAlias(false); |
| paint.setStyle(SkPaint::kFill_Style); |
| paint.setColor(CurrentFrameColor()); |
| gfx::Path path; |
| path.moveTo(0, 0); |
| path.lineTo(width(), 0); |
| path.lineTo(width(), kCaptionHeight); |
| path.lineTo(0, kCaptionHeight); |
| path.close(); |
| canvas->DrawPath(path, paint); |
| } |
| |
| const char* AppWindowFrameView::GetClassName() const { return kViewClassName; } |
| |
| gfx::Size AppWindowFrameView::GetMinimumSize() const { |
| gfx::Size min_size = widget_->client_view()->GetMinimumSize(); |
| if (!draw_frame_) { |
| min_size.SetToMax(gfx::Size(1, 1)); |
| return min_size; |
| } |
| |
| // Ensure we can display the top of the caption area. |
| gfx::Rect client_bounds = GetBoundsForClientView(); |
| min_size.Enlarge(0, client_bounds.y()); |
| // Ensure we have enough space for the window icon and buttons. We allow |
| // the title string to collapse to zero width. |
| int closeButtonOffsetX = (kCaptionHeight - close_button_->height()) / 2; |
| int header_width = close_button_->width() + closeButtonOffsetX * 2; |
| if (header_width > min_size.width()) |
| min_size.set_width(header_width); |
| return min_size; |
| } |
| |
| gfx::Size AppWindowFrameView::GetMaximumSize() const { |
| gfx::Size max_size = widget_->client_view()->GetMaximumSize(); |
| |
| // Add to the client maximum size the height of any title bar and borders. |
| gfx::Size client_size = GetBoundsForClientView().size(); |
| if (max_size.width()) |
| max_size.Enlarge(width() - client_size.width(), 0); |
| if (max_size.height()) |
| max_size.Enlarge(0, height() - client_size.height()); |
| |
| return max_size; |
| } |
| |
| void AppWindowFrameView::ButtonPressed(views::Button* sender, |
| const ui::Event& event) { |
| DCHECK(draw_frame_); |
| if (sender == close_button_) |
| widget_->Close(); |
| else if (sender == maximize_button_) |
| widget_->Maximize(); |
| else if (sender == restore_button_) |
| widget_->Restore(); |
| else if (sender == minimize_button_) |
| widget_->Minimize(); |
| } |
| |
| SkColor AppWindowFrameView::CurrentFrameColor() { |
| return widget_->IsActive() ? active_frame_color_ : inactive_frame_color_; |
| } |
| |
| void AppWindowFrameView::SetButtonImagesForFrame() { |
| DCHECK(draw_frame_); |
| |
| // If the frame is dark, we should use the light images so they have |
| // some contrast. |
| const uint8_t kLumaThreshold = 100; |
| bool use_light = color_utils::GetLuma(CurrentFrameColor()) < kLumaThreshold; |
| |
| ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); |
| if (use_light) { |
| maximize_button_->SetImage( |
| views::CustomButton::STATE_NORMAL, |
| rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_L).ToImageSkia()); |
| restore_button_->SetImage( |
| views::CustomButton::STATE_NORMAL, |
| rb.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE_L).ToImageSkia()); |
| minimize_button_->SetImage( |
| views::CustomButton::STATE_NORMAL, |
| rb.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE_L).ToImageSkia()); |
| } else { |
| maximize_button_->SetImage( |
| views::CustomButton::STATE_NORMAL, |
| rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE).ToImageSkia()); |
| restore_button_->SetImage( |
| views::CustomButton::STATE_NORMAL, |
| rb.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE).ToImageSkia()); |
| minimize_button_->SetImage( |
| views::CustomButton::STATE_NORMAL, |
| rb.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE).ToImageSkia()); |
| } |
| } |
| |
| } // namespace apps |