| // Copyright (c) 2009 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/views/fullscreen_exit_bubble.h" |
| |
| #include "app/keyboard_codes.h" |
| #include "app/l10n_util.h" |
| #include "app/resource_bundle.h" |
| #include "base/utf_string_conversions.h" |
| #include "chrome/app/chrome_command_ids.h" |
| #include "gfx/canvas_skia.h" |
| #include "grit/generated_resources.h" |
| #include "ui/base/animation/slide_animation.h" |
| #include "views/screen.h" |
| #include "views/widget/root_view.h" |
| #include "views/window/window.h" |
| |
| #if defined(OS_WIN) |
| #include "app/l10n_util_win.h" |
| #include "views/widget/widget_win.h" |
| #elif defined(OS_LINUX) |
| #include "views/widget/widget_gtk.h" |
| #endif |
| |
| // FullscreenExitView ---------------------------------------------------------- |
| |
| class FullscreenExitBubble::FullscreenExitView : public views::View { |
| public: |
| FullscreenExitView(FullscreenExitBubble* bubble, |
| const std::wstring& accelerator); |
| virtual ~FullscreenExitView(); |
| |
| // views::View |
| virtual gfx::Size GetPreferredSize(); |
| |
| private: |
| static const int kPaddingPixels; // Number of pixels around all sides of link |
| |
| // views::View |
| virtual void Layout(); |
| virtual void Paint(gfx::Canvas* canvas); |
| |
| // Clickable hint text to show in the bubble. |
| views::Link link_; |
| }; |
| |
| const int FullscreenExitBubble::FullscreenExitView::kPaddingPixels = 8; |
| |
| FullscreenExitBubble::FullscreenExitView::FullscreenExitView( |
| FullscreenExitBubble* bubble, |
| const std::wstring& accelerator) { |
| link_.set_parent_owned(false); |
| #if !defined(OS_CHROMEOS) |
| link_.SetText( |
| UTF16ToWide(l10n_util::GetStringFUTF16(IDS_EXIT_FULLSCREEN_MODE, |
| WideToUTF16(accelerator)))); |
| #else |
| link_.SetText( |
| UTF16ToWide(l10n_util::GetStringUTF16(IDS_EXIT_FULLSCREEN_MODE))); |
| #endif |
| link_.SetController(bubble); |
| link_.SetFont(ResourceBundle::GetSharedInstance().GetFont( |
| ResourceBundle::LargeFont)); |
| link_.SetNormalColor(SK_ColorWHITE); |
| link_.SetHighlightedColor(SK_ColorWHITE); |
| AddChildView(&link_); |
| } |
| |
| FullscreenExitBubble::FullscreenExitView::~FullscreenExitView() { |
| } |
| |
| gfx::Size FullscreenExitBubble::FullscreenExitView::GetPreferredSize() { |
| gfx::Size preferred_size(link_.GetPreferredSize()); |
| preferred_size.Enlarge(kPaddingPixels * 2, kPaddingPixels * 2); |
| return preferred_size; |
| } |
| |
| void FullscreenExitBubble::FullscreenExitView::Layout() { |
| gfx::Size link_preferred_size(link_.GetPreferredSize()); |
| link_.SetBounds(kPaddingPixels, |
| height() - kPaddingPixels - link_preferred_size.height(), |
| link_preferred_size.width(), link_preferred_size.height()); |
| } |
| |
| void FullscreenExitBubble::FullscreenExitView::Paint(gfx::Canvas* canvas) { |
| // Create a round-bottomed rect to fill the whole View. |
| SkRect rect; |
| SkScalar padding = SkIntToScalar(kPaddingPixels); |
| // The "-padding" top coordinate ensures that the rect is always tall enough |
| // to contain the complete rounded corner radius. If we set this to 0, as the |
| // popup slides offscreen (in reality, squishes to 0 height), the corners will |
| // flatten out as the height becomes less than the corner radius. |
| rect.set(0, -padding, SkIntToScalar(width()), SkIntToScalar(height())); |
| SkScalar rad[8] = { 0, 0, 0, 0, padding, padding, padding, padding }; |
| SkPath path; |
| path.addRoundRect(rect, rad, SkPath::kCW_Direction); |
| |
| // Fill it black. |
| SkPaint paint; |
| paint.setStyle(SkPaint::kFill_Style); |
| paint.setFlags(SkPaint::kAntiAlias_Flag); |
| paint.setColor(SK_ColorBLACK); |
| canvas->AsCanvasSkia()->drawPath(path, paint); |
| } |
| |
| |
| // FullscreenExitPopup --------------------------------------------------------- |
| |
| #if defined(OS_WIN) |
| class FullscreenExitBubble::FullscreenExitPopup : public views::WidgetWin { |
| public: |
| FullscreenExitPopup() : views::WidgetWin() {} |
| virtual ~FullscreenExitPopup() {} |
| |
| // views::WidgetWin: |
| virtual LRESULT OnMouseActivate(HWND window, |
| UINT hittest_code, |
| UINT message) { |
| // Prevent the popup from being activated, so it won't steal focus from the |
| // rest of the browser, and doesn't cause problems with the FocusManager's |
| // "RestoreFocusedView()" functionality. |
| return MA_NOACTIVATE; |
| } |
| }; |
| #elif defined(OS_LINUX) |
| // TODO: figure out the equivalent of MA_NOACTIVATE for gtk. |
| #endif |
| |
| // FullscreenExitBubble -------------------------------------------------------- |
| |
| const double FullscreenExitBubble::kOpacity = 0.7; |
| const int FullscreenExitBubble::kInitialDelayMs = 2300; |
| const int FullscreenExitBubble::kIdleTimeMs = 2300; |
| const int FullscreenExitBubble::kPositionCheckHz = 10; |
| const int FullscreenExitBubble::kSlideInRegionHeightPx = 4; |
| const int FullscreenExitBubble::kSlideInDurationMs = 350; |
| const int FullscreenExitBubble::kSlideOutDurationMs = 700; |
| |
| FullscreenExitBubble::FullscreenExitBubble( |
| views::Widget* frame, |
| CommandUpdater::CommandUpdaterDelegate* delegate) |
| : root_view_(frame->GetRootView()), |
| delegate_(delegate), |
| popup_(NULL), |
| size_animation_(new ui::SlideAnimation(this)) { |
| size_animation_->Reset(1); |
| |
| // Create the contents view. |
| views::Accelerator accelerator(app::VKEY_UNKNOWN, false, false, false); |
| bool got_accelerator = frame->GetAccelerator(IDC_FULLSCREEN, &accelerator); |
| DCHECK(got_accelerator); |
| view_ = new FullscreenExitView(this, accelerator.GetShortcutText()); |
| |
| // Initialize the popup. |
| #if defined(OS_WIN) |
| popup_ = new FullscreenExitPopup(); |
| popup_->set_window_style(WS_POPUP); |
| popup_->set_window_ex_style(WS_EX_LAYERED | WS_EX_TOOLWINDOW | |
| l10n_util::GetExtendedTooltipStyles()); |
| #elif defined(OS_LINUX) |
| popup_ = new views::WidgetGtk(views::WidgetGtk::TYPE_POPUP); |
| popup_->MakeTransparent(); |
| #endif |
| popup_->SetOpacity(static_cast<unsigned char>(0xff * kOpacity)); |
| popup_->Init(frame->GetNativeView(), GetPopupRect(false)); |
| popup_->set_delete_on_destroy(false); |
| popup_->SetContentsView(view_); |
| popup_->Show(); // This does not activate the popup. |
| |
| // Start the initial delay timer and begin watching the mouse. |
| initial_delay_.Start(base::TimeDelta::FromMilliseconds(kInitialDelayMs), this, |
| &FullscreenExitBubble::CheckMousePosition); |
| gfx::Point cursor_pos = views::Screen::GetCursorScreenPoint(); |
| last_mouse_pos_ = cursor_pos; |
| views::View::ConvertPointToView(NULL, root_view_, &last_mouse_pos_); |
| mouse_position_checker_.Start( |
| base::TimeDelta::FromMilliseconds(1000 / kPositionCheckHz), this, |
| &FullscreenExitBubble::CheckMousePosition); |
| } |
| |
| FullscreenExitBubble::~FullscreenExitBubble() { |
| // This is tricky. We may be in an ATL message handler stack, in which case |
| // the popup cannot be deleted yet. We also can't blindly use |
| // set_delete_on_destroy(true) on the popup to delete it when it closes, |
| // because if the user closed the last tab while in fullscreen mode, Windows |
| // has already destroyed the popup HWND by the time we get here, and thus |
| // either the popup will already have been deleted (if we set this in our |
| // constructor) or the popup will never get another OnFinalMessage() call (if |
| // not, as currently). So instead, we tell the popup to synchronously hide, |
| // and then asynchronously close and delete itself. |
| popup_->Close(); |
| MessageLoop::current()->DeleteSoon(FROM_HERE, popup_); |
| } |
| |
| void FullscreenExitBubble::LinkActivated(views::Link* source, int event_flags) { |
| delegate_->ExecuteCommand(IDC_FULLSCREEN); |
| } |
| |
| void FullscreenExitBubble::AnimationProgressed( |
| const ui::Animation* animation) { |
| gfx::Rect popup_rect(GetPopupRect(false)); |
| if (popup_rect.IsEmpty()) { |
| popup_->Hide(); |
| } else { |
| #if defined(OS_WIN) |
| popup_->MoveWindow(popup_rect.x(), popup_rect.y(), popup_rect.width(), |
| popup_rect.height()); |
| #elif defined(OS_LINUX) |
| popup_->SetBounds(popup_rect); |
| #endif |
| popup_->Show(); |
| } |
| } |
| void FullscreenExitBubble::AnimationEnded( |
| const ui::Animation* animation) { |
| AnimationProgressed(animation); |
| } |
| |
| void FullscreenExitBubble::CheckMousePosition() { |
| // Desired behavior: |
| // |
| // +------------+-----------------------------+------------+ |
| // | _ _ _ _ | Exit full screen mode (F11) | _ _ _ _ | Slide-in region |
| // | _ _ _ _ \_____________________________/ _ _ _ _ | Neutral region |
| // | | Slide-out region |
| // : : |
| // |
| // * If app is not active, we hide the popup. |
| // * If the mouse is offscreen or in the slide-out region, we hide the popup. |
| // * If the mouse goes idle, we hide the popup. |
| // * If the mouse is in the slide-in-region and not idle, we show the popup. |
| // * If the mouse is in the neutral region and not idle, and the popup is |
| // currently sliding out, we show it again. This facilitates users |
| // correcting us if they try to mouse horizontally towards the popup and |
| // unintentionally drop too low. |
| // * Otherwise, we do nothing, because the mouse is in the neutral region and |
| // either the popup is hidden or the mouse is not idle, so we don't want to |
| // change anything's state. |
| |
| gfx::Point cursor_pos = views::Screen::GetCursorScreenPoint(); |
| gfx::Point transformed_pos(cursor_pos); |
| views::View::ConvertPointToView(NULL, root_view_, &transformed_pos); |
| |
| // Check to see whether the mouse is idle. |
| if (transformed_pos != last_mouse_pos_) { |
| // The mouse moved; reset the idle timer. |
| idle_timeout_.Stop(); // If the timer isn't running, this is a no-op. |
| idle_timeout_.Start(base::TimeDelta::FromMilliseconds(kIdleTimeMs), this, |
| &FullscreenExitBubble::CheckMousePosition); |
| } |
| last_mouse_pos_ = transformed_pos; |
| |
| if ((!root_view_->GetWidget()->IsActive()) || |
| !root_view_->HitTest(transformed_pos) || |
| (cursor_pos.y() >= GetPopupRect(true).bottom()) || |
| !idle_timeout_.IsRunning()) { |
| // The cursor is offscreen, in the slide-out region, or idle. |
| Hide(); |
| } else if ((cursor_pos.y() < kSlideInRegionHeightPx) || |
| (size_animation_->GetCurrentValue() != 0)) { |
| // The cursor is not idle, and either it's in the slide-in region or it's in |
| // the neutral region and we're sliding out. |
| size_animation_->SetSlideDuration(kSlideInDurationMs); |
| size_animation_->Show(); |
| } |
| } |
| |
| void FullscreenExitBubble::Hide() { |
| // Allow the bubble to hide if the window is deactivated or our initial delay |
| // finishes. |
| if ((!root_view_->GetWidget()->IsActive()) || !initial_delay_.IsRunning()) { |
| size_animation_->SetSlideDuration(kSlideOutDurationMs); |
| size_animation_->Hide(); |
| } |
| } |
| |
| gfx::Rect FullscreenExitBubble::GetPopupRect( |
| bool ignore_animation_state) const { |
| gfx::Size size(view_->GetPreferredSize()); |
| if (!ignore_animation_state) { |
| size.set_height(static_cast<int>(static_cast<double>(size.height()) * |
| size_animation_->GetCurrentValue())); |
| } |
| // NOTE: don't use the bounds of the root_view_. On linux changing window |
| // size is async. Instead we use the size of the screen. |
| gfx::Rect screen_bounds = views::Screen::GetMonitorAreaNearestWindow( |
| root_view_->GetWidget()->GetNativeView()); |
| gfx::Point origin(screen_bounds.x() + |
| (screen_bounds.width() - size.width()) / 2, |
| screen_bounds.y()); |
| return gfx::Rect(origin, size); |
| } |