blob: 6d940a460065c32a977771a60d51601869cf6854 [file] [log] [blame]
// 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 <memory>
#include <utility>
#include "base/strings/utf_string_conversions.h"
#include "build/chromeos_buildflags.h"
#include "cc/paint/paint_flags.h"
#include "chrome/grit/theme_resources.h"
#include "extensions/browser/app_window/native_app_window.h"
#include "extensions/common/draggable_region.h"
#include "third_party/skia/include/core/SkPath.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/metadata/metadata_impl_macros.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/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 kCaptionHeight = 25;
} // namespace
namespace apps {
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) {}
AppWindowFrameView::~AppWindowFrameView() = default;
void AppWindowFrameView::Init() {
if (draw_frame_) {
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
auto close_button = std::make_unique<views::ImageButton>(
base::BindRepeating(&views::Widget::Close, base::Unretained(widget_)));
close_button->SetImage(
views::Button::STATE_NORMAL,
rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE).ToImageSkia());
close_button->SetImage(
views::Button::STATE_HOVERED,
rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE_H).ToImageSkia());
close_button->SetImage(
views::Button::STATE_PRESSED,
rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE_P).ToImageSkia());
close_button->SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
close_button->SetAccessibleName(
l10n_util::GetStringUTF16(IDS_APP_ACCNAME_CLOSE));
close_button_ = AddChildView(std::move(close_button));
// STATE_NORMAL images are set in SetButtonImagesForFrame, not here.
auto maximize_button =
std::make_unique<views::ImageButton>(base::BindRepeating(
&views::Widget::Maximize, base::Unretained(widget_)));
maximize_button->SetImage(
views::Button::STATE_HOVERED,
rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_H).ToImageSkia());
maximize_button->SetImage(
views::Button::STATE_PRESSED,
rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_P).ToImageSkia());
maximize_button->SetImage(
views::Button::STATE_DISABLED,
rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_D).ToImageSkia());
maximize_button->SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
maximize_button->SetAccessibleName(
l10n_util::GetStringUTF16(IDS_APP_ACCNAME_MAXIMIZE));
maximize_button_ = AddChildView(std::move(maximize_button));
auto restore_button =
std::make_unique<views::ImageButton>(base::BindRepeating(
&views::Widget::Restore, base::Unretained(widget_)));
restore_button->SetImage(
views::Button::STATE_HOVERED,
rb.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE_H).ToImageSkia());
restore_button->SetImage(
views::Button::STATE_PRESSED,
rb.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE_P).ToImageSkia());
restore_button->SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
restore_button->SetAccessibleName(
l10n_util::GetStringUTF16(IDS_APP_ACCNAME_RESTORE));
restore_button_ = AddChildView(std::move(restore_button));
auto minimize_button =
std::make_unique<views::ImageButton>(base::BindRepeating(
&views::Widget::Minimize, base::Unretained(widget_)));
minimize_button->SetImage(
views::Button::STATE_HOVERED,
rb.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE_H).ToImageSkia());
minimize_button->SetImage(
views::Button::STATE_PRESSED,
rb.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE_P).ToImageSkia());
minimize_button->SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
minimize_button->SetAccessibleName(
l10n_util::GetStringUTF16(IDS_APP_ACCNAME_MINIMIZE));
minimize_button_ = AddChildView(std::move(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;
// TODO(crbug.com/1052397): Revisit once build flag switch of lacros-chrome is
// complete.
#if defined(OS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS)
// 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_->GetVisible() &&
close_button_->GetMirroredBounds().Contains(point)) {
return HTCLOSE;
}
if ((maximize_button_ && maximize_button_->GetVisible() &&
maximize_button_->GetMirroredBounds().Contains(point)) ||
(restore_button_ && restore_button_->GetVisible() &&
restore_button_->GetMirroredBounds().Contains(point))) {
return HTMAXBUTTON;
}
if (minimize_button_ && minimize_button_->GetVisible() &&
minimize_button_->GetMirroredBounds().Contains(point)) {
return HTMINBUTTON;
}
// Caption is a safe default.
return HTCAPTION;
}
void AppWindowFrameView::GetWindowMask(const gfx::Size& size,
SkPath* 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::CalculatePreferredSize() 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() {
NonClientFrameView::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::Button::STATE_NORMAL);
else
restore_button_->SetState(views::Button::STATE_NORMAL);
gfx::Size minimize_size = minimize_button_->GetPreferredSize();
minimize_button_->SetState(views::Button::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::Button::STATE_NORMAL,
rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE).ToImageSkia());
} else {
close_button_->SetImage(
views::Button::STATE_NORMAL,
rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE_U).ToImageSkia());
}
SetButtonImagesForFrame();
// TODO(benwells): different look for inactive by default.
cc::PaintFlags flags;
flags.setAntiAlias(false);
flags.setStyle(cc::PaintFlags::kFill_Style);
flags.setColor(CurrentFrameColor());
SkPath path;
path.moveTo(0, 0);
path.lineTo(width(), 0);
path.lineTo(width(), kCaptionHeight);
path.lineTo(0, kCaptionHeight);
path.close();
canvas->DrawPath(path, flags);
}
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;
}
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.
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
if (color_utils::IsDark(CurrentFrameColor())) {
maximize_button_->SetImage(
views::Button::STATE_NORMAL,
rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_L).ToImageSkia());
restore_button_->SetImage(
views::Button::STATE_NORMAL,
rb.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE_L).ToImageSkia());
minimize_button_->SetImage(
views::Button::STATE_NORMAL,
rb.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE_L).ToImageSkia());
} else {
maximize_button_->SetImage(
views::Button::STATE_NORMAL,
rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE).ToImageSkia());
restore_button_->SetImage(
views::Button::STATE_NORMAL,
rb.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE).ToImageSkia());
minimize_button_->SetImage(
views::Button::STATE_NORMAL,
rb.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE).ToImageSkia());
}
}
BEGIN_METADATA(AppWindowFrameView, views::NonClientFrameView)
END_METADATA
} // namespace apps