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