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 28 class 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 48 const int FullscreenExitBubble::FullscreenExitView::kPaddingPixels = 8; 49 50 FullscreenExitBubble::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 70 FullscreenExitBubble::FullscreenExitView::~FullscreenExitView() { 71 } 72 73 gfx::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 79 void 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 86 void 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 109 const double FullscreenExitBubble::kOpacity = 0.7; 110 const int FullscreenExitBubble::kInitialDelayMs = 2300; 111 const int FullscreenExitBubble::kIdleTimeMs = 2300; 112 const int FullscreenExitBubble::kPositionCheckHz = 10; 113 const int FullscreenExitBubble::kSlideInRegionHeightPx = 4; 114 const int FullscreenExitBubble::kSlideInDurationMs = 350; 115 const int FullscreenExitBubble::kSlideOutDurationMs = 700; 116 117 FullscreenExitBubble::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 155 FullscreenExitBubble::~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 169 void FullscreenExitBubble::LinkActivated(views::Link* source, int event_flags) { 170 delegate_->ExecuteCommand(IDC_FULLSCREEN); 171 } 172 173 void 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 } 183 void FullscreenExitBubble::AnimationEnded( 184 const ui::Animation* animation) { 185 AnimationProgressed(animation); 186 } 187 188 void 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 237 void 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 246 gfx::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