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