1// Copyright (c) 2011 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 "chrome/browser/ui/views/fullscreen_exit_bubble.h"
6
7#include "base/utf_string_conversions.h"
8#include "chrome/app/chrome_command_ids.h"
9#include "grit/generated_resources.h"
10#include "ui/base/animation/slide_animation.h"
11#include "ui/base/keycodes/keyboard_codes.h"
12#include "ui/base/l10n/l10n_util.h"
13#include "ui/base/resource/resource_bundle.h"
14#include "ui/gfx/canvas_skia.h"
15#include "views/screen.h"
16#include "views/widget/root_view.h"
17#include "views/window/window.h"
18
19#if defined(OS_WIN)
20#include "ui/base/l10n/l10n_util_win.h"
21#include "views/widget/widget_win.h"
22#elif defined(OS_LINUX)
23#include "views/widget/widget_gtk.h"
24#endif
25
26// FullscreenExitView ----------------------------------------------------------
27
28class FullscreenExitBubble::FullscreenExitView : public views::View {
29 public:
30  FullscreenExitView(FullscreenExitBubble* bubble,
31                     const std::wstring& accelerator);
32  virtual ~FullscreenExitView();
33
34  // views::View
35  virtual gfx::Size GetPreferredSize();
36
37 private:
38  static const int kPaddingPixels;  // Number of pixels around all sides of link
39
40  // views::View
41  virtual void Layout();
42  virtual void OnPaint(gfx::Canvas* canvas);
43
44  // Clickable hint text to show in the bubble.
45  views::Link link_;
46};
47
48const int FullscreenExitBubble::FullscreenExitView::kPaddingPixels = 8;
49
50FullscreenExitBubble::FullscreenExitView::FullscreenExitView(
51    FullscreenExitBubble* bubble,
52    const std::wstring& accelerator) {
53  link_.set_parent_owned(false);
54#if !defined(OS_CHROMEOS)
55  link_.SetText(
56      UTF16ToWide(l10n_util::GetStringFUTF16(IDS_EXIT_FULLSCREEN_MODE,
57                                             WideToUTF16(accelerator))));
58#else
59  link_.SetText(
60      UTF16ToWide(l10n_util::GetStringUTF16(IDS_EXIT_FULLSCREEN_MODE)));
61#endif
62  link_.SetController(bubble);
63  link_.SetFont(ResourceBundle::GetSharedInstance().GetFont(
64      ResourceBundle::LargeFont));
65  link_.SetNormalColor(SK_ColorWHITE);
66  link_.SetHighlightedColor(SK_ColorWHITE);
67  AddChildView(&link_);
68}
69
70FullscreenExitBubble::FullscreenExitView::~FullscreenExitView() {
71}
72
73gfx::Size FullscreenExitBubble::FullscreenExitView::GetPreferredSize() {
74  gfx::Size preferred_size(link_.GetPreferredSize());
75  preferred_size.Enlarge(kPaddingPixels * 2, kPaddingPixels * 2);
76  return preferred_size;
77}
78
79void FullscreenExitBubble::FullscreenExitView::Layout() {
80  gfx::Size link_preferred_size(link_.GetPreferredSize());
81  link_.SetBounds(kPaddingPixels,
82                  height() - kPaddingPixels - link_preferred_size.height(),
83                  link_preferred_size.width(), link_preferred_size.height());
84}
85
86void FullscreenExitBubble::FullscreenExitView::OnPaint(gfx::Canvas* canvas) {
87  // Create a round-bottomed rect to fill the whole View.
88  SkRect rect;
89  SkScalar padding = SkIntToScalar(kPaddingPixels);
90  // The "-padding" top coordinate ensures that the rect is always tall enough
91  // to contain the complete rounded corner radius.  If we set this to 0, as the
92  // popup slides offscreen (in reality, squishes to 0 height), the corners will
93  // flatten out as the height becomes less than the corner radius.
94  rect.set(0, -padding, SkIntToScalar(width()), SkIntToScalar(height()));
95  SkScalar rad[8] = { 0, 0, 0, 0, padding, padding, padding, padding };
96  SkPath path;
97  path.addRoundRect(rect, rad, SkPath::kCW_Direction);
98
99  // Fill it black.
100  SkPaint paint;
101  paint.setStyle(SkPaint::kFill_Style);
102  paint.setFlags(SkPaint::kAntiAlias_Flag);
103  paint.setColor(SK_ColorBLACK);
104  canvas->AsCanvasSkia()->drawPath(path, paint);
105}
106
107// FullscreenExitBubble --------------------------------------------------------
108
109const double FullscreenExitBubble::kOpacity = 0.7;
110const int FullscreenExitBubble::kInitialDelayMs = 2300;
111const int FullscreenExitBubble::kIdleTimeMs = 2300;
112const int FullscreenExitBubble::kPositionCheckHz = 10;
113const int FullscreenExitBubble::kSlideInRegionHeightPx = 4;
114const int FullscreenExitBubble::kSlideInDurationMs = 350;
115const int FullscreenExitBubble::kSlideOutDurationMs = 700;
116
117FullscreenExitBubble::FullscreenExitBubble(
118    views::Widget* frame,
119    CommandUpdater::CommandUpdaterDelegate* delegate)
120    : root_view_(frame->GetRootView()),
121      delegate_(delegate),
122      popup_(NULL),
123      size_animation_(new ui::SlideAnimation(this)) {
124  size_animation_->Reset(1);
125
126  // Create the contents view.
127  views::Accelerator accelerator(ui::VKEY_UNKNOWN, false, false, false);
128  bool got_accelerator = frame->GetAccelerator(IDC_FULLSCREEN, &accelerator);
129  DCHECK(got_accelerator);
130  view_ = new FullscreenExitView(
131      this, UTF16ToWideHack(accelerator.GetShortcutText()));
132
133  // Initialize the popup.
134  views::Widget::CreateParams params(views::Widget::CreateParams::TYPE_POPUP);
135  params.transparent = true;
136  params.can_activate = false;
137  params.delete_on_destroy = false;
138  popup_ = views::Widget::CreateWidget(params);
139  popup_->SetOpacity(static_cast<unsigned char>(0xff * kOpacity));
140  popup_->Init(frame->GetNativeView(), GetPopupRect(false));
141  popup_->SetContentsView(view_);
142  popup_->Show();  // This does not activate the popup.
143
144  // Start the initial delay timer and begin watching the mouse.
145  initial_delay_.Start(base::TimeDelta::FromMilliseconds(kInitialDelayMs), this,
146                       &FullscreenExitBubble::CheckMousePosition);
147  gfx::Point cursor_pos = views::Screen::GetCursorScreenPoint();
148  last_mouse_pos_ = cursor_pos;
149  views::View::ConvertPointToView(NULL, root_view_, &last_mouse_pos_);
150  mouse_position_checker_.Start(
151      base::TimeDelta::FromMilliseconds(1000 / kPositionCheckHz), this,
152      &FullscreenExitBubble::CheckMousePosition);
153}
154
155FullscreenExitBubble::~FullscreenExitBubble() {
156  // This is tricky.  We may be in an ATL message handler stack, in which case
157  // the popup cannot be deleted yet.  We also can't blindly use
158  // set_delete_on_destroy(true) on the popup to delete it when it closes,
159  // because if the user closed the last tab while in fullscreen mode, Windows
160  // has already destroyed the popup HWND by the time we get here, and thus
161  // either the popup will already have been deleted (if we set this in our
162  // constructor) or the popup will never get another OnFinalMessage() call (if
163  // not, as currently).  So instead, we tell the popup to synchronously hide,
164  // and then asynchronously close and delete itself.
165  popup_->Close();
166  MessageLoop::current()->DeleteSoon(FROM_HERE, popup_);
167}
168
169void FullscreenExitBubble::LinkActivated(views::Link* source, int event_flags) {
170  delegate_->ExecuteCommand(IDC_FULLSCREEN);
171}
172
173void FullscreenExitBubble::AnimationProgressed(
174    const ui::Animation* animation) {
175  gfx::Rect popup_rect(GetPopupRect(false));
176  if (popup_rect.IsEmpty()) {
177    popup_->Hide();
178  } else {
179    popup_->SetBounds(popup_rect);
180    popup_->Show();
181  }
182}
183void FullscreenExitBubble::AnimationEnded(
184    const ui::Animation* animation) {
185  AnimationProgressed(animation);
186}
187
188void FullscreenExitBubble::CheckMousePosition() {
189  // Desired behavior:
190  //
191  // +------------+-----------------------------+------------+
192  // | _  _  _  _ | Exit full screen mode (F11) | _  _  _  _ |  Slide-in region
193  // | _  _  _  _ \_____________________________/ _  _  _  _ |  Neutral region
194  // |                                                       |  Slide-out region
195  // :                                                       :
196  //
197  // * If app is not active, we hide the popup.
198  // * If the mouse is offscreen or in the slide-out region, we hide the popup.
199  // * If the mouse goes idle, we hide the popup.
200  // * If the mouse is in the slide-in-region and not idle, we show the popup.
201  // * If the mouse is in the neutral region and not idle, and the popup is
202  //   currently sliding out, we show it again.  This facilitates users
203  //   correcting us if they try to mouse horizontally towards the popup and
204  //   unintentionally drop too low.
205  // * Otherwise, we do nothing, because the mouse is in the neutral region and
206  //   either the popup is hidden or the mouse is not idle, so we don't want to
207  //   change anything's state.
208
209  gfx::Point cursor_pos = views::Screen::GetCursorScreenPoint();
210  gfx::Point transformed_pos(cursor_pos);
211  views::View::ConvertPointToView(NULL, root_view_, &transformed_pos);
212
213  // Check to see whether the mouse is idle.
214  if (transformed_pos != last_mouse_pos_) {
215    // The mouse moved; reset the idle timer.
216    idle_timeout_.Stop();  // If the timer isn't running, this is a no-op.
217    idle_timeout_.Start(base::TimeDelta::FromMilliseconds(kIdleTimeMs), this,
218                        &FullscreenExitBubble::CheckMousePosition);
219  }
220  last_mouse_pos_ = transformed_pos;
221
222  if ((!root_view_->GetWidget()->IsActive()) ||
223      !root_view_->HitTest(transformed_pos) ||
224      (cursor_pos.y() >= GetPopupRect(true).bottom()) ||
225      !idle_timeout_.IsRunning()) {
226    // The cursor is offscreen, in the slide-out region, or idle.
227    Hide();
228  } else if ((cursor_pos.y() < kSlideInRegionHeightPx) ||
229             (size_animation_->GetCurrentValue() != 0)) {
230    // The cursor is not idle, and either it's in the slide-in region or it's in
231    // the neutral region and we're sliding out.
232    size_animation_->SetSlideDuration(kSlideInDurationMs);
233    size_animation_->Show();
234  }
235}
236
237void FullscreenExitBubble::Hide() {
238  // Allow the bubble to hide if the window is deactivated or our initial delay
239  // finishes.
240  if ((!root_view_->GetWidget()->IsActive()) || !initial_delay_.IsRunning()) {
241    size_animation_->SetSlideDuration(kSlideOutDurationMs);
242    size_animation_->Hide();
243  }
244}
245
246gfx::Rect FullscreenExitBubble::GetPopupRect(
247    bool ignore_animation_state) const {
248  gfx::Size size(view_->GetPreferredSize());
249  if (!ignore_animation_state) {
250    size.set_height(static_cast<int>(static_cast<double>(size.height()) *
251        size_animation_->GetCurrentValue()));
252  }
253  // NOTE: don't use the bounds of the root_view_. On linux changing window
254  // size is async. Instead we use the size of the screen.
255  gfx::Rect screen_bounds = views::Screen::GetMonitorAreaNearestWindow(
256      root_view_->GetWidget()->GetNativeView());
257  gfx::Point origin(screen_bounds.x() +
258                    (screen_bounds.width() - size.width()) / 2,
259                    screen_bounds.y());
260  return gfx::Rect(origin, size);
261}
262