1// Copyright 2013 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 "apps/ui/views/shell_window_frame_view.h"
6
7#include "apps/ui/native_app_window.h"
8#include "base/strings/utf_string_conversions.h"
9#include "extensions/common/draggable_region.h"
10#include "grit/theme_resources.h"
11#include "grit/ui_strings.h"  // Accessibility names
12#include "third_party/skia/include/core/SkPaint.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/image/image.h"
18#include "ui/gfx/path.h"
19#include "ui/views/controls/button/image_button.h"
20#include "ui/views/layout/grid_layout.h"
21#include "ui/views/views_delegate.h"
22#include "ui/views/widget/widget.h"
23#include "ui/views/widget/widget_delegate.h"
24
25#if defined(USE_AURA)
26#include "ui/aura/env.h"
27#include "ui/aura/window.h"
28#endif
29
30namespace {
31// Height of the chrome-style caption, in pixels.
32const int kCaptionHeight = 25;
33}  // namespace
34
35namespace apps {
36
37const char ShellWindowFrameView::kViewClassName[] =
38    "browser/ui/views/extensions/ShellWindowFrameView";
39
40ShellWindowFrameView::ShellWindowFrameView(NativeAppWindow* window)
41    : window_(window),
42      frame_(NULL),
43      close_button_(NULL),
44      maximize_button_(NULL),
45      restore_button_(NULL),
46      minimize_button_(NULL),
47      resize_inside_bounds_size_(0),
48      resize_area_corner_size_(0) {
49}
50
51ShellWindowFrameView::~ShellWindowFrameView() {
52}
53
54void ShellWindowFrameView::Init(views::Widget* frame,
55                                int resize_inside_bounds_size,
56                                int resize_outside_bounds_size,
57                                int resize_outside_scale_for_touch,
58                                int resize_area_corner_size) {
59  frame_ = frame;
60  resize_inside_bounds_size_ = resize_inside_bounds_size;
61  resize_area_corner_size_ = resize_area_corner_size;
62
63  if (!window_->IsFrameless()) {
64    ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
65    close_button_ = new views::ImageButton(this);
66    close_button_->SetImage(views::CustomButton::STATE_NORMAL,
67        rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE).ToImageSkia());
68    close_button_->SetImage(views::CustomButton::STATE_HOVERED,
69        rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE_H).ToImageSkia());
70    close_button_->SetImage(views::CustomButton::STATE_PRESSED,
71        rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE_P).ToImageSkia());
72    close_button_->SetAccessibleName(
73        l10n_util::GetStringUTF16(IDS_APP_ACCNAME_CLOSE));
74    AddChildView(close_button_);
75    maximize_button_ = new views::ImageButton(this);
76    maximize_button_->SetImage(views::CustomButton::STATE_NORMAL,
77        rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE).ToImageSkia());
78    maximize_button_->SetImage(views::CustomButton::STATE_HOVERED,
79        rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_H).ToImageSkia());
80    maximize_button_->SetImage(views::CustomButton::STATE_PRESSED,
81        rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_P).ToImageSkia());
82    maximize_button_->SetImage(views::CustomButton::STATE_DISABLED,
83        rb.GetNativeImageNamed(IDR_APP_WINDOW_MAXIMIZE_D).ToImageSkia());
84    maximize_button_->SetAccessibleName(
85        l10n_util::GetStringUTF16(IDS_APP_ACCNAME_MAXIMIZE));
86    AddChildView(maximize_button_);
87    restore_button_ = new views::ImageButton(this);
88    restore_button_->SetImage(views::CustomButton::STATE_NORMAL,
89        rb.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE).ToImageSkia());
90    restore_button_->SetImage(views::CustomButton::STATE_HOVERED,
91        rb.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE_H).ToImageSkia());
92    restore_button_->SetImage(views::CustomButton::STATE_PRESSED,
93        rb.GetNativeImageNamed(IDR_APP_WINDOW_RESTORE_P).ToImageSkia());
94    restore_button_->SetAccessibleName(
95        l10n_util::GetStringUTF16(IDS_APP_ACCNAME_RESTORE));
96    AddChildView(restore_button_);
97    minimize_button_ = new views::ImageButton(this);
98    minimize_button_->SetImage(views::CustomButton::STATE_NORMAL,
99        rb.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE).ToImageSkia());
100    minimize_button_->SetImage(views::CustomButton::STATE_HOVERED,
101        rb.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE_H).ToImageSkia());
102    minimize_button_->SetImage(views::CustomButton::STATE_PRESSED,
103        rb.GetNativeImageNamed(IDR_APP_WINDOW_MINIMIZE_P).ToImageSkia());
104    minimize_button_->SetAccessibleName(
105        l10n_util::GetStringUTF16(IDS_APP_ACCNAME_MINIMIZE));
106    AddChildView(minimize_button_);
107  }
108
109#if defined(USE_AURA)
110  aura::Window* window = frame->GetNativeWindow();
111  // Some Aura implementations (Ash) allow resize handles outside the window.
112  if (resize_outside_bounds_size > 0) {
113    gfx::Insets mouse_insets = gfx::Insets(-resize_outside_bounds_size,
114                                           -resize_outside_bounds_size,
115                                           -resize_outside_bounds_size,
116                                           -resize_outside_bounds_size);
117    gfx::Insets touch_insets =
118        mouse_insets.Scale(resize_outside_scale_for_touch);
119    // Ensure we get resize cursors for a few pixels outside our bounds.
120    window->SetHitTestBoundsOverrideOuter(mouse_insets, touch_insets);
121  }
122  // Ensure we get resize cursors just inside our bounds as well.
123  // TODO(jeremya): do we need to update these when in fullscreen/maximized?
124  window->set_hit_test_bounds_override_inner(
125      gfx::Insets(resize_inside_bounds_size_, resize_inside_bounds_size_,
126                  resize_inside_bounds_size_, resize_inside_bounds_size_));
127#endif
128}
129
130// views::NonClientFrameView implementation.
131
132gfx::Rect ShellWindowFrameView::GetBoundsForClientView() const {
133  if (window_->IsFrameless() || frame_->IsFullscreen())
134    return bounds();
135  return gfx::Rect(0, kCaptionHeight, width(),
136      std::max(0, height() - kCaptionHeight));
137}
138
139gfx::Rect ShellWindowFrameView::GetWindowBoundsForClientBounds(
140      const gfx::Rect& client_bounds) const {
141  if (window_->IsFrameless()) {
142    gfx::Rect window_bounds = client_bounds;
143    // Enforce minimum size (1, 1) in case that client_bounds is passed with
144    // empty size. This could occur when the frameless window is being
145    // initialized.
146    if (window_bounds.IsEmpty()) {
147      window_bounds.set_width(1);
148      window_bounds.set_height(1);
149    }
150    return window_bounds;
151  }
152
153  int closeButtonOffsetX =
154      (kCaptionHeight - close_button_->height()) / 2;
155  int header_width = close_button_->width() + closeButtonOffsetX * 2;
156  return gfx::Rect(client_bounds.x(),
157                   std::max(0, client_bounds.y() - kCaptionHeight),
158                   std::max(header_width, client_bounds.width()),
159                   client_bounds.height() + kCaptionHeight);
160}
161
162int ShellWindowFrameView::NonClientHitTest(const gfx::Point& point) {
163  if (frame_->IsFullscreen())
164    return HTCLIENT;
165
166  gfx::Rect expanded_bounds = bounds();
167#if defined(USE_AURA)
168  // Some Aura implementations (Ash) optionally allow resize handles just
169  // outside the window bounds.
170  aura::Window* window = frame_->GetNativeWindow();
171  if (aura::Env::GetInstance()->is_touch_down())
172    expanded_bounds.Inset(window->hit_test_bounds_override_outer_touch());
173  else
174    expanded_bounds.Inset(window->hit_test_bounds_override_outer_mouse());
175#endif
176  // Points outside the (possibly expanded) bounds can be discarded.
177  if (!expanded_bounds.Contains(point))
178    return HTNOWHERE;
179
180  // Check the frame first, as we allow a small area overlapping the contents
181  // to be used for resize handles.
182  bool can_ever_resize = frame_->widget_delegate() ?
183      frame_->widget_delegate()->CanResize() :
184      false;
185  // Don't allow overlapping resize handles when the window is maximized or
186  // fullscreen, as it can't be resized in those states.
187  int resize_border =
188      (frame_->IsMaximized() || frame_->IsFullscreen()) ? 0 :
189      resize_inside_bounds_size_;
190  int frame_component = GetHTComponentForFrame(point,
191                                               resize_border,
192                                               resize_border,
193                                               resize_area_corner_size_,
194                                               resize_area_corner_size_,
195                                               can_ever_resize);
196  if (frame_component != HTNOWHERE)
197    return frame_component;
198
199  // Check for possible draggable region in the client area for the frameless
200  // window.
201  if (window_->IsFrameless()) {
202    SkRegion* draggable_region = window_->GetDraggableRegion();
203    if (draggable_region && draggable_region->contains(point.x(), point.y()))
204      return HTCAPTION;
205  }
206
207  int client_component = frame_->client_view()->NonClientHitTest(point);
208  if (client_component != HTNOWHERE)
209    return client_component;
210
211  // Then see if the point is within any of the window controls.
212  if (close_button_ && close_button_->visible() &&
213      close_button_->GetMirroredBounds().Contains(point)) {
214    return HTCLOSE;
215  }
216  if ((maximize_button_ && maximize_button_->visible() &&
217       maximize_button_->GetMirroredBounds().Contains(point)) ||
218      (restore_button_ && restore_button_->visible() &&
219       restore_button_->GetMirroredBounds().Contains(point))) {
220    return HTMAXBUTTON;
221  }
222  if (minimize_button_ && minimize_button_->visible() &&
223      minimize_button_->GetMirroredBounds().Contains(point)) {
224    return HTMINBUTTON;
225  }
226
227  // Caption is a safe default.
228  return HTCAPTION;
229}
230
231void ShellWindowFrameView::GetWindowMask(const gfx::Size& size,
232                                         gfx::Path* window_mask) {
233  // We got nothing to say about no window mask.
234}
235
236// views::View implementation.
237
238gfx::Size ShellWindowFrameView::GetPreferredSize() {
239  gfx::Size pref = frame_->client_view()->GetPreferredSize();
240  gfx::Rect bounds(0, 0, pref.width(), pref.height());
241  return frame_->non_client_view()->GetWindowBoundsForClientBounds(
242      bounds).size();
243}
244
245void ShellWindowFrameView::Layout() {
246  if (window_->IsFrameless())
247    return;
248  gfx::Size close_size = close_button_->GetPreferredSize();
249  const int kButtonOffsetY = 0;
250  const int kButtonSpacing = 1;
251  const int kRightMargin = 3;
252
253  close_button_->SetBounds(
254      width() - kRightMargin - close_size.width(),
255      kButtonOffsetY,
256      close_size.width(),
257      close_size.height());
258
259  bool can_ever_resize = frame_->widget_delegate() ?
260      frame_->widget_delegate()->CanResize() :
261      false;
262  maximize_button_->SetEnabled(can_ever_resize);
263  gfx::Size maximize_size = maximize_button_->GetPreferredSize();
264  maximize_button_->SetBounds(
265      close_button_->x() - kButtonSpacing - maximize_size.width(),
266      kButtonOffsetY,
267      maximize_size.width(),
268      maximize_size.height());
269  gfx::Size restore_size = restore_button_->GetPreferredSize();
270  restore_button_->SetBounds(
271      close_button_->x() - kButtonSpacing - restore_size.width(),
272      kButtonOffsetY,
273      restore_size.width(),
274      restore_size.height());
275
276  bool maximized = frame_->IsMaximized();
277  maximize_button_->SetVisible(!maximized);
278  restore_button_->SetVisible(maximized);
279  if (maximized)
280    maximize_button_->SetState(views::CustomButton::STATE_NORMAL);
281  else
282    restore_button_->SetState(views::CustomButton::STATE_NORMAL);
283
284  gfx::Size minimize_size = minimize_button_->GetPreferredSize();
285  minimize_button_->SetBounds(
286      maximize_button_->x() - kButtonSpacing - minimize_size.width(),
287      kButtonOffsetY,
288      minimize_size.width(),
289      minimize_size.height());
290}
291
292void ShellWindowFrameView::OnPaint(gfx::Canvas* canvas) {
293  if (window_->IsFrameless())
294    return;
295
296  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
297  if (ShouldPaintAsActive()) {
298    close_button_->SetImage(views::CustomButton::STATE_NORMAL,
299        rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE).ToImageSkia());
300  } else {
301    close_button_->SetImage(views::CustomButton::STATE_NORMAL,
302        rb.GetNativeImageNamed(IDR_APP_WINDOW_CLOSE_U).ToImageSkia());
303  }
304
305  // TODO(jeremya): different look for inactive?
306  SkPaint paint;
307  paint.setAntiAlias(false);
308  paint.setStyle(SkPaint::kFill_Style);
309  paint.setColor(SK_ColorWHITE);
310  gfx::Path path;
311  const int radius = frame_->IsMaximized() ? 0 : 1;
312  path.moveTo(0, radius);
313  path.lineTo(radius, 0);
314  path.lineTo(width() - radius - 1, 0);
315  path.lineTo(width(), radius + 1);
316  path.lineTo(width(), kCaptionHeight);
317  path.lineTo(0, kCaptionHeight);
318  path.close();
319  canvas->DrawPath(path, paint);
320}
321
322const char* ShellWindowFrameView::GetClassName() const {
323  return kViewClassName;
324}
325
326gfx::Size ShellWindowFrameView::GetMinimumSize() {
327  gfx::Size min_size = frame_->client_view()->GetMinimumSize();
328  if (window_->IsFrameless())
329    return min_size;
330
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 =
337      (kCaptionHeight - close_button_->height()) / 2;
338  int header_width = close_button_->width() + closeButtonOffsetX * 2;
339  if (header_width > min_size.width())
340    min_size.set_width(header_width);
341  return min_size;
342}
343
344gfx::Size ShellWindowFrameView::GetMaximumSize() {
345  gfx::Size max_size = frame_->client_view()->GetMaximumSize();
346
347  // Add to the client maximum size the height of any title bar and borders.
348  gfx::Size client_size = GetBoundsForClientView().size();
349  if (max_size.width())
350    max_size.Enlarge(width() - client_size.width(), 0);
351  if (max_size.height())
352    max_size.Enlarge(0, height() - client_size.height());
353
354  return max_size;
355}
356
357// views::ButtonListener implementation.
358
359void ShellWindowFrameView::ButtonPressed(views::Button* sender,
360                                         const ui::Event& event) {
361  DCHECK(!window_->IsFrameless());
362  if (sender == close_button_)
363    frame_->Close();
364  else if (sender == maximize_button_)
365    frame_->Maximize();
366  else if (sender == restore_button_)
367    frame_->Restore();
368  else if (sender == minimize_button_)
369    frame_->Minimize();
370}
371
372}  // namespace apps
373