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/views_delegate.h"
24 #include "ui/views/widget/widget.h"
25 #include "ui/views/widget/widget_delegate.h"
29 const int kDefaultResizeInsideBoundsSize
= 5;
30 const int kDefaultResizeAreaCornerSize
= 16;
31 const int kCaptionHeight
= 25;
37 const char AppWindowFrameView::kViewClassName
[] =
38 "browser/ui/views/extensions/AppWindowFrameView";
40 AppWindowFrameView::AppWindowFrameView(views::Widget
* widget
,
41 extensions::NativeAppWindow
* window
,
43 const SkColor
& active_frame_color
,
44 const SkColor
& inactive_frame_color
)
47 draw_frame_(draw_frame
),
48 active_frame_color_(active_frame_color
),
49 inactive_frame_color_(inactive_frame_color
),
51 maximize_button_(NULL
),
52 restore_button_(NULL
),
53 minimize_button_(NULL
),
54 resize_inside_bounds_size_(kDefaultResizeInsideBoundsSize
),
55 resize_outside_bounds_size_(0),
56 resize_area_corner_size_(kDefaultResizeAreaCornerSize
) {
59 AppWindowFrameView::~AppWindowFrameView() {}
61 void AppWindowFrameView::Init() {
63 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
64 close_button_
= new views::ImageButton(this);
65 close_button_
->SetImage(
66 views::CustomButton::STATE_NORMAL
,
67 rb
.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE
).ToImageSkia());
68 close_button_
->SetImage(
69 views::CustomButton::STATE_HOVERED
,
70 rb
.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE_H
).ToImageSkia());
71 close_button_
->SetImage(
72 views::CustomButton::STATE_PRESSED
,
73 rb
.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE_P
).ToImageSkia());
74 close_button_
->SetAccessibleName(
75 l10n_util::GetStringUTF16(IDS_APP_ACCNAME_CLOSE
));
76 AddChildView(close_button_
);
77 // STATE_NORMAL images are set in SetButtonImagesForFrame, not here.
78 maximize_button_
= new views::ImageButton(this);
79 maximize_button_
->SetImage(
80 views::CustomButton::STATE_HOVERED
,
81 rb
.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_H
).ToImageSkia());
82 maximize_button_
->SetImage(
83 views::CustomButton::STATE_PRESSED
,
84 rb
.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_P
).ToImageSkia());
85 maximize_button_
->SetImage(
86 views::CustomButton::STATE_DISABLED
,
87 rb
.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_D
).ToImageSkia());
88 maximize_button_
->SetAccessibleName(
89 l10n_util::GetStringUTF16(IDS_APP_ACCNAME_MAXIMIZE
));
90 AddChildView(maximize_button_
);
91 restore_button_
= new views::ImageButton(this);
92 restore_button_
->SetImage(
93 views::CustomButton::STATE_HOVERED
,
94 rb
.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE_H
).ToImageSkia());
95 restore_button_
->SetImage(
96 views::CustomButton::STATE_PRESSED
,
97 rb
.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE_P
).ToImageSkia());
98 restore_button_
->SetAccessibleName(
99 l10n_util::GetStringUTF16(IDS_APP_ACCNAME_RESTORE
));
100 AddChildView(restore_button_
);
101 minimize_button_
= new views::ImageButton(this);
102 minimize_button_
->SetImage(
103 views::CustomButton::STATE_HOVERED
,
104 rb
.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE_H
).ToImageSkia());
105 minimize_button_
->SetImage(
106 views::CustomButton::STATE_PRESSED
,
107 rb
.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE_P
).ToImageSkia());
108 minimize_button_
->SetAccessibleName(
109 l10n_util::GetStringUTF16(IDS_APP_ACCNAME_MINIMIZE
));
110 AddChildView(minimize_button_
);
112 SetButtonImagesForFrame();
116 void AppWindowFrameView::SetResizeSizes(int resize_inside_bounds_size
,
117 int resize_outside_bounds_size
,
118 int resize_area_corner_size
) {
119 resize_inside_bounds_size_
= resize_inside_bounds_size
;
120 resize_outside_bounds_size_
= resize_outside_bounds_size
;
121 resize_area_corner_size_
= resize_area_corner_size
;
124 // views::NonClientFrameView implementation.
126 gfx::Rect
AppWindowFrameView::GetBoundsForClientView() const {
127 if (!draw_frame_
|| widget_
->IsFullscreen())
130 0, kCaptionHeight
, width(), std::max(0, height() - kCaptionHeight
));
133 gfx::Rect
AppWindowFrameView::GetWindowBoundsForClientBounds(
134 const gfx::Rect
& client_bounds
) const {
135 gfx::Rect window_bounds
= client_bounds
;
136 #if defined(OS_LINUX) && !defined(OS_CHROMEOS)
137 // Get the difference between the widget's client area bounds and window
138 // bounds, and grow |window_bounds| by that amount.
139 gfx::Insets native_frame_insets
=
140 widget_
->GetClientAreaBoundsInScreen().InsetsFrom(
141 widget_
->GetWindowBoundsInScreen());
142 window_bounds
.Inset(native_frame_insets
);
145 // Enforce minimum size (1, 1) in case that client_bounds is passed with
146 // empty size. This could occur when the frameless window is being
148 if (window_bounds
.IsEmpty()) {
149 window_bounds
.set_width(1);
150 window_bounds
.set_height(1);
152 return window_bounds
;
155 int closeButtonOffsetX
= (kCaptionHeight
- close_button_
->height()) / 2;
156 int header_width
= close_button_
->width() + closeButtonOffsetX
* 2;
157 return gfx::Rect(window_bounds
.x(),
158 window_bounds
.y() - kCaptionHeight
,
159 std::max(header_width
, window_bounds
.width()),
160 window_bounds
.height() + kCaptionHeight
);
163 int AppWindowFrameView::NonClientHitTest(const gfx::Point
& point
) {
164 if (widget_
->IsFullscreen())
167 gfx::Rect expanded_bounds
= bounds();
168 if (resize_outside_bounds_size_
) {
169 expanded_bounds
.Inset(gfx::Insets(-resize_outside_bounds_size_
,
170 -resize_outside_bounds_size_
,
171 -resize_outside_bounds_size_
,
172 -resize_outside_bounds_size_
));
174 // Points outside the (possibly expanded) bounds can be discarded.
175 if (!expanded_bounds
.Contains(point
))
178 // Check the frame first, as we allow a small area overlapping the contents
179 // to be used for resize handles.
180 bool can_ever_resize
= widget_
->widget_delegate()
181 ? widget_
->widget_delegate()->CanResize()
183 // Don't allow overlapping resize handles when the window is maximized or
184 // fullscreen, as it can't be resized in those states.
185 int resize_border
= (widget_
->IsMaximized() || widget_
->IsFullscreen())
187 : resize_inside_bounds_size_
;
188 int frame_component
= GetHTComponentForFrame(point
,
191 resize_area_corner_size_
,
192 resize_area_corner_size_
,
194 if (frame_component
!= HTNOWHERE
)
195 return frame_component
;
197 // Check for possible draggable region in the client area for the frameless
199 SkRegion
* draggable_region
= window_
->GetDraggableRegion();
200 if (draggable_region
&& draggable_region
->contains(point
.x(), point
.y()))
203 int client_component
= widget_
->client_view()->NonClientHitTest(point
);
204 if (client_component
!= HTNOWHERE
)
205 return client_component
;
207 // Then see if the point is within any of the window controls.
208 if (close_button_
&& close_button_
->visible() &&
209 close_button_
->GetMirroredBounds().Contains(point
)) {
212 if ((maximize_button_
&& maximize_button_
->visible() &&
213 maximize_button_
->GetMirroredBounds().Contains(point
)) ||
214 (restore_button_
&& restore_button_
->visible() &&
215 restore_button_
->GetMirroredBounds().Contains(point
))) {
218 if (minimize_button_
&& minimize_button_
->visible() &&
219 minimize_button_
->GetMirroredBounds().Contains(point
)) {
223 // Caption is a safe default.
227 void AppWindowFrameView::GetWindowMask(const gfx::Size
& size
,
228 gfx::Path
* window_mask
) {
229 // We got nothing to say about no window mask.
232 void AppWindowFrameView::SizeConstraintsChanged() {
234 maximize_button_
->SetEnabled(widget_
->widget_delegate() &&
235 widget_
->widget_delegate()->CanMaximize());
239 gfx::Size
AppWindowFrameView::GetPreferredSize() const {
240 gfx::Size pref
= widget_
->client_view()->GetPreferredSize();
241 gfx::Rect
bounds(0, 0, pref
.width(), pref
.height());
242 return widget_
->non_client_view()
243 ->GetWindowBoundsForClientBounds(bounds
)
247 void AppWindowFrameView::Layout() {
250 gfx::Size close_size
= close_button_
->GetPreferredSize();
251 const int kButtonOffsetY
= 0;
252 const int kButtonSpacing
= 1;
253 const int kRightMargin
= 3;
255 close_button_
->SetBounds(width() - kRightMargin
- close_size
.width(),
258 close_size
.height());
260 maximize_button_
->SetEnabled(widget_
->widget_delegate() &&
261 widget_
->widget_delegate()->CanMaximize());
262 gfx::Size maximize_size
= maximize_button_
->GetPreferredSize();
263 maximize_button_
->SetBounds(
264 close_button_
->x() - kButtonSpacing
- maximize_size
.width(),
266 maximize_size
.width(),
267 maximize_size
.height());
268 gfx::Size restore_size
= restore_button_
->GetPreferredSize();
269 restore_button_
->SetBounds(
270 close_button_
->x() - kButtonSpacing
- restore_size
.width(),
272 restore_size
.width(),
273 restore_size
.height());
275 bool maximized
= widget_
->IsMaximized();
276 maximize_button_
->SetVisible(!maximized
);
277 restore_button_
->SetVisible(maximized
);
279 maximize_button_
->SetState(views::CustomButton::STATE_NORMAL
);
281 restore_button_
->SetState(views::CustomButton::STATE_NORMAL
);
283 gfx::Size minimize_size
= minimize_button_
->GetPreferredSize();
284 minimize_button_
->SetState(views::CustomButton::STATE_NORMAL
);
285 minimize_button_
->SetBounds(
286 maximize_button_
->x() - kButtonSpacing
- minimize_size
.width(),
288 minimize_size
.width(),
289 minimize_size
.height());
292 void AppWindowFrameView::OnPaint(gfx::Canvas
* canvas
) {
296 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
297 if (ShouldPaintAsActive()) {
298 close_button_
->SetImage(
299 views::CustomButton::STATE_NORMAL
,
300 rb
.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE
).ToImageSkia());
302 close_button_
->SetImage(
303 views::CustomButton::STATE_NORMAL
,
304 rb
.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE_U
).ToImageSkia());
307 SetButtonImagesForFrame();
308 // TODO(benwells): different look for inactive by default.
310 paint
.setAntiAlias(false);
311 paint
.setStyle(SkPaint::kFill_Style
);
312 paint
.setColor(CurrentFrameColor());
315 path
.lineTo(width(), 0);
316 path
.lineTo(width(), kCaptionHeight
);
317 path
.lineTo(0, kCaptionHeight
);
319 canvas
->DrawPath(path
, paint
);
322 const char* AppWindowFrameView::GetClassName() const { return kViewClassName
; }
324 gfx::Size
AppWindowFrameView::GetMinimumSize() const {
325 gfx::Size min_size
= widget_
->client_view()->GetMinimumSize();
327 min_size
.SetToMax(gfx::Size(1, 1));
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
= (kCaptionHeight
- close_button_
->height()) / 2;
337 int header_width
= close_button_
->width() + closeButtonOffsetX
* 2;
338 if (header_width
> min_size
.width())
339 min_size
.set_width(header_width
);
343 gfx::Size
AppWindowFrameView::GetMaximumSize() const {
344 gfx::Size max_size
= widget_
->client_view()->GetMaximumSize();
346 // Add to the client maximum size the height of any title bar and borders.
347 gfx::Size client_size
= GetBoundsForClientView().size();
348 if (max_size
.width())
349 max_size
.Enlarge(width() - client_size
.width(), 0);
350 if (max_size
.height())
351 max_size
.Enlarge(0, height() - client_size
.height());
356 void AppWindowFrameView::ButtonPressed(views::Button
* sender
,
357 const ui::Event
& event
) {
359 if (sender
== close_button_
)
361 else if (sender
== maximize_button_
)
363 else if (sender
== restore_button_
)
365 else if (sender
== minimize_button_
)
369 SkColor
AppWindowFrameView::CurrentFrameColor() {
370 return widget_
->IsActive() ? active_frame_color_
: inactive_frame_color_
;
373 void AppWindowFrameView::SetButtonImagesForFrame() {
376 // If the frame is dark, we should use the light images so they have
378 unsigned char frame_luma
=
379 color_utils::GetLuminanceForColor(CurrentFrameColor());
380 const unsigned char kLuminanceThreshold
= 100;
381 bool use_light
= frame_luma
< kLuminanceThreshold
;
383 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
385 maximize_button_
->SetImage(
386 views::CustomButton::STATE_NORMAL
,
387 rb
.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_L
).ToImageSkia());
388 restore_button_
->SetImage(
389 views::CustomButton::STATE_NORMAL
,
390 rb
.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE_L
).ToImageSkia());
391 minimize_button_
->SetImage(
392 views::CustomButton::STATE_NORMAL
,
393 rb
.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE_L
).ToImageSkia());
395 maximize_button_
->SetImage(
396 views::CustomButton::STATE_NORMAL
,
397 rb
.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE
).ToImageSkia());
398 restore_button_
->SetImage(
399 views::CustomButton::STATE_NORMAL
,
400 rb
.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE
).ToImageSkia());
401 minimize_button_
->SetImage(
402 views::CustomButton::STATE_NORMAL
,
403 rb
.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE
).ToImageSkia());