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.
5 #include "apps/ui/views/app_window_frame_view.h"
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/widget/widget.h"
24 #include "ui/views/widget/widget_delegate.h"
28 const int kDefaultResizeInsideBoundsSize
= 5;
29 const int kDefaultResizeAreaCornerSize
= 16;
30 const int kCaptionHeight
= 25;
36 const char AppWindowFrameView::kViewClassName
[] =
37 "browser/ui/views/extensions/AppWindowFrameView";
39 AppWindowFrameView::AppWindowFrameView(views::Widget
* widget
,
40 extensions::NativeAppWindow
* window
,
42 const SkColor
& active_frame_color
,
43 const SkColor
& inactive_frame_color
)
46 draw_frame_(draw_frame
),
47 active_frame_color_(active_frame_color
),
48 inactive_frame_color_(inactive_frame_color
),
50 maximize_button_(NULL
),
51 restore_button_(NULL
),
52 minimize_button_(NULL
),
53 resize_inside_bounds_size_(kDefaultResizeInsideBoundsSize
),
54 resize_outside_bounds_size_(0),
55 resize_area_corner_size_(kDefaultResizeAreaCornerSize
) {
58 AppWindowFrameView::~AppWindowFrameView() {}
60 void AppWindowFrameView::Init() {
62 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
63 close_button_
= new views::ImageButton(this);
64 close_button_
->SetImage(
65 views::CustomButton::STATE_NORMAL
,
66 rb
.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE
).ToImageSkia());
67 close_button_
->SetImage(
68 views::CustomButton::STATE_HOVERED
,
69 rb
.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE_H
).ToImageSkia());
70 close_button_
->SetImage(
71 views::CustomButton::STATE_PRESSED
,
72 rb
.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE_P
).ToImageSkia());
73 close_button_
->SetAccessibleName(
74 l10n_util::GetStringUTF16(IDS_APP_ACCNAME_CLOSE
));
75 AddChildView(close_button_
);
76 // STATE_NORMAL images are set in SetButtonImagesForFrame, not here.
77 maximize_button_
= new views::ImageButton(this);
78 maximize_button_
->SetImage(
79 views::CustomButton::STATE_HOVERED
,
80 rb
.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_H
).ToImageSkia());
81 maximize_button_
->SetImage(
82 views::CustomButton::STATE_PRESSED
,
83 rb
.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_P
).ToImageSkia());
84 maximize_button_
->SetImage(
85 views::CustomButton::STATE_DISABLED
,
86 rb
.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_D
).ToImageSkia());
87 maximize_button_
->SetAccessibleName(
88 l10n_util::GetStringUTF16(IDS_APP_ACCNAME_MAXIMIZE
));
89 AddChildView(maximize_button_
);
90 restore_button_
= new views::ImageButton(this);
91 restore_button_
->SetImage(
92 views::CustomButton::STATE_HOVERED
,
93 rb
.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE_H
).ToImageSkia());
94 restore_button_
->SetImage(
95 views::CustomButton::STATE_PRESSED
,
96 rb
.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE_P
).ToImageSkia());
97 restore_button_
->SetAccessibleName(
98 l10n_util::GetStringUTF16(IDS_APP_ACCNAME_RESTORE
));
99 AddChildView(restore_button_
);
100 minimize_button_
= new views::ImageButton(this);
101 minimize_button_
->SetImage(
102 views::CustomButton::STATE_HOVERED
,
103 rb
.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE_H
).ToImageSkia());
104 minimize_button_
->SetImage(
105 views::CustomButton::STATE_PRESSED
,
106 rb
.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE_P
).ToImageSkia());
107 minimize_button_
->SetAccessibleName(
108 l10n_util::GetStringUTF16(IDS_APP_ACCNAME_MINIMIZE
));
109 AddChildView(minimize_button_
);
111 SetButtonImagesForFrame();
115 void AppWindowFrameView::SetResizeSizes(int resize_inside_bounds_size
,
116 int resize_outside_bounds_size
,
117 int resize_area_corner_size
) {
118 resize_inside_bounds_size_
= resize_inside_bounds_size
;
119 resize_outside_bounds_size_
= resize_outside_bounds_size
;
120 resize_area_corner_size_
= resize_area_corner_size
;
123 // views::NonClientFrameView implementation.
125 gfx::Rect
AppWindowFrameView::GetBoundsForClientView() const {
126 if (!draw_frame_
|| widget_
->IsFullscreen())
129 0, kCaptionHeight
, width(), std::max(0, height() - kCaptionHeight
));
132 gfx::Rect
AppWindowFrameView::GetWindowBoundsForClientBounds(
133 const gfx::Rect
& client_bounds
) const {
134 gfx::Rect window_bounds
= client_bounds
;
135 #if defined(OS_LINUX) && !defined(OS_CHROMEOS)
136 // Get the difference between the widget's client area bounds and window
137 // bounds, and grow |window_bounds| by that amount.
138 gfx::Insets native_frame_insets
=
139 widget_
->GetClientAreaBoundsInScreen().InsetsFrom(
140 widget_
->GetWindowBoundsInScreen());
141 window_bounds
.Inset(native_frame_insets
);
144 // Enforce minimum size (1, 1) in case that client_bounds is passed with
145 // empty size. This could occur when the frameless window is being
147 if (window_bounds
.IsEmpty()) {
148 window_bounds
.set_width(1);
149 window_bounds
.set_height(1);
151 return window_bounds
;
154 int closeButtonOffsetX
= (kCaptionHeight
- close_button_
->height()) / 2;
155 int header_width
= close_button_
->width() + closeButtonOffsetX
* 2;
156 return gfx::Rect(window_bounds
.x(),
157 window_bounds
.y() - kCaptionHeight
,
158 std::max(header_width
, window_bounds
.width()),
159 window_bounds
.height() + kCaptionHeight
);
162 int AppWindowFrameView::NonClientHitTest(const gfx::Point
& point
) {
163 if (widget_
->IsFullscreen())
166 gfx::Rect expanded_bounds
= bounds();
167 if (resize_outside_bounds_size_
) {
168 expanded_bounds
.Inset(gfx::Insets(-resize_outside_bounds_size_
,
169 -resize_outside_bounds_size_
,
170 -resize_outside_bounds_size_
,
171 -resize_outside_bounds_size_
));
173 // Points outside the (possibly expanded) bounds can be discarded.
174 if (!expanded_bounds
.Contains(point
))
177 // Check the frame first, as we allow a small area overlapping the contents
178 // to be used for resize handles.
179 bool can_ever_resize
= widget_
->widget_delegate()
180 ? widget_
->widget_delegate()->CanResize()
182 // Don't allow overlapping resize handles when the window is maximized or
183 // fullscreen, as it can't be resized in those states.
184 int resize_border
= (widget_
->IsMaximized() || widget_
->IsFullscreen())
186 : resize_inside_bounds_size_
;
187 int frame_component
= GetHTComponentForFrame(point
,
190 resize_area_corner_size_
,
191 resize_area_corner_size_
,
193 if (frame_component
!= HTNOWHERE
)
194 return frame_component
;
196 // Check for possible draggable region in the client area for the frameless
198 SkRegion
* draggable_region
= window_
->GetDraggableRegion();
199 if (draggable_region
&& draggable_region
->contains(point
.x(), point
.y()))
202 int client_component
= widget_
->client_view()->NonClientHitTest(point
);
203 if (client_component
!= HTNOWHERE
)
204 return client_component
;
206 // Then see if the point is within any of the window controls.
207 if (close_button_
&& close_button_
->visible() &&
208 close_button_
->GetMirroredBounds().Contains(point
)) {
211 if ((maximize_button_
&& maximize_button_
->visible() &&
212 maximize_button_
->GetMirroredBounds().Contains(point
)) ||
213 (restore_button_
&& restore_button_
->visible() &&
214 restore_button_
->GetMirroredBounds().Contains(point
))) {
217 if (minimize_button_
&& minimize_button_
->visible() &&
218 minimize_button_
->GetMirroredBounds().Contains(point
)) {
222 // Caption is a safe default.
226 void AppWindowFrameView::GetWindowMask(const gfx::Size
& size
,
227 gfx::Path
* window_mask
) {
228 // We got nothing to say about no window mask.
231 void AppWindowFrameView::SizeConstraintsChanged() {
233 maximize_button_
->SetEnabled(widget_
->widget_delegate() &&
234 widget_
->widget_delegate()->CanMaximize());
238 gfx::Size
AppWindowFrameView::GetPreferredSize() const {
239 gfx::Size pref
= widget_
->client_view()->GetPreferredSize();
240 gfx::Rect
bounds(0, 0, pref
.width(), pref
.height());
241 return widget_
->non_client_view()
242 ->GetWindowBoundsForClientBounds(bounds
)
246 void AppWindowFrameView::Layout() {
249 gfx::Size close_size
= close_button_
->GetPreferredSize();
250 const int kButtonOffsetY
= 0;
251 const int kButtonSpacing
= 1;
252 const int kRightMargin
= 3;
254 close_button_
->SetBounds(width() - kRightMargin
- close_size
.width(),
257 close_size
.height());
259 maximize_button_
->SetEnabled(widget_
->widget_delegate() &&
260 widget_
->widget_delegate()->CanMaximize());
261 gfx::Size maximize_size
= maximize_button_
->GetPreferredSize();
262 maximize_button_
->SetBounds(
263 close_button_
->x() - kButtonSpacing
- maximize_size
.width(),
265 maximize_size
.width(),
266 maximize_size
.height());
267 gfx::Size restore_size
= restore_button_
->GetPreferredSize();
268 restore_button_
->SetBounds(
269 close_button_
->x() - kButtonSpacing
- restore_size
.width(),
271 restore_size
.width(),
272 restore_size
.height());
274 bool maximized
= widget_
->IsMaximized();
275 maximize_button_
->SetVisible(!maximized
);
276 restore_button_
->SetVisible(maximized
);
278 maximize_button_
->SetState(views::CustomButton::STATE_NORMAL
);
280 restore_button_
->SetState(views::CustomButton::STATE_NORMAL
);
282 gfx::Size minimize_size
= minimize_button_
->GetPreferredSize();
283 minimize_button_
->SetState(views::CustomButton::STATE_NORMAL
);
284 minimize_button_
->SetBounds(
285 maximize_button_
->x() - kButtonSpacing
- minimize_size
.width(),
287 minimize_size
.width(),
288 minimize_size
.height());
291 void AppWindowFrameView::OnPaint(gfx::Canvas
* canvas
) {
295 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
296 if (ShouldPaintAsActive()) {
297 close_button_
->SetImage(
298 views::CustomButton::STATE_NORMAL
,
299 rb
.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE
).ToImageSkia());
301 close_button_
->SetImage(
302 views::CustomButton::STATE_NORMAL
,
303 rb
.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE_U
).ToImageSkia());
306 SetButtonImagesForFrame();
307 // TODO(benwells): different look for inactive by default.
309 paint
.setAntiAlias(false);
310 paint
.setStyle(SkPaint::kFill_Style
);
311 paint
.setColor(CurrentFrameColor());
314 path
.lineTo(width(), 0);
315 path
.lineTo(width(), kCaptionHeight
);
316 path
.lineTo(0, kCaptionHeight
);
318 canvas
->DrawPath(path
, paint
);
321 const char* AppWindowFrameView::GetClassName() const { return kViewClassName
; }
323 gfx::Size
AppWindowFrameView::GetMinimumSize() const {
324 gfx::Size min_size
= widget_
->client_view()->GetMinimumSize();
326 min_size
.SetToMax(gfx::Size(1, 1));
330 // Ensure we can display the top of the caption area.
331 gfx::Rect client_bounds
= GetBoundsForClientView();
332 min_size
.Enlarge(0, client_bounds
.y());
333 // Ensure we have enough space for the window icon and buttons. We allow
334 // the title string to collapse to zero width.
335 int closeButtonOffsetX
= (kCaptionHeight
- close_button_
->height()) / 2;
336 int header_width
= close_button_
->width() + closeButtonOffsetX
* 2;
337 if (header_width
> min_size
.width())
338 min_size
.set_width(header_width
);
342 gfx::Size
AppWindowFrameView::GetMaximumSize() const {
343 gfx::Size max_size
= widget_
->client_view()->GetMaximumSize();
345 // Add to the client maximum size the height of any title bar and borders.
346 gfx::Size client_size
= GetBoundsForClientView().size();
347 if (max_size
.width())
348 max_size
.Enlarge(width() - client_size
.width(), 0);
349 if (max_size
.height())
350 max_size
.Enlarge(0, height() - client_size
.height());
355 void AppWindowFrameView::ButtonPressed(views::Button
* sender
,
356 const ui::Event
& event
) {
358 if (sender
== close_button_
)
360 else if (sender
== maximize_button_
)
362 else if (sender
== restore_button_
)
364 else if (sender
== minimize_button_
)
368 SkColor
AppWindowFrameView::CurrentFrameColor() {
369 return widget_
->IsActive() ? active_frame_color_
: inactive_frame_color_
;
372 void AppWindowFrameView::SetButtonImagesForFrame() {
375 // If the frame is dark, we should use the light images so they have
377 unsigned char frame_luma
=
378 color_utils::GetLuminanceForColor(CurrentFrameColor());
379 const unsigned char kLuminanceThreshold
= 100;
380 bool use_light
= frame_luma
< kLuminanceThreshold
;
382 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
384 maximize_button_
->SetImage(
385 views::CustomButton::STATE_NORMAL
,
386 rb
.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_L
).ToImageSkia());
387 restore_button_
->SetImage(
388 views::CustomButton::STATE_NORMAL
,
389 rb
.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE_L
).ToImageSkia());
390 minimize_button_
->SetImage(
391 views::CustomButton::STATE_NORMAL
,
392 rb
.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE_L
).ToImageSkia());
394 maximize_button_
->SetImage(
395 views::CustomButton::STATE_NORMAL
,
396 rb
.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE
).ToImageSkia());
397 restore_button_
->SetImage(
398 views::CustomButton::STATE_NORMAL
,
399 rb
.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE
).ToImageSkia());
400 minimize_button_
->SetImage(
401 views::CustomButton::STATE_NORMAL
,
402 rb
.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE
).ToImageSkia());