Home | History | Annotate | Download | only in views
      1 // Copyright (c) 2012 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_views.h"
      6 
      7 #include "base/message_loop/message_loop.h"
      8 #include "base/strings/utf_string_conversions.h"
      9 #include "chrome/app/chrome_command_ids.h"
     10 #include "chrome/browser/chrome_notification_types.h"
     11 #include "chrome/browser/ui/fullscreen/fullscreen_controller.h"
     12 #include "chrome/browser/ui/views/frame/browser_view.h"
     13 #include "chrome/browser/ui/views/frame/immersive_mode_controller.h"
     14 #include "chrome/browser/ui/views/frame/top_container_view.h"
     15 #include "content/public/browser/notification_service.h"
     16 #include "grit/generated_resources.h"
     17 #include "grit/ui_strings.h"
     18 #include "ui/base/animation/slide_animation.h"
     19 #include "ui/base/keycodes/keyboard_codes.h"
     20 #include "ui/base/l10n/l10n_util.h"
     21 #include "ui/base/resource/resource_bundle.h"
     22 #include "ui/gfx/canvas.h"
     23 #include "ui/gfx/screen.h"
     24 #include "ui/views/bubble/bubble_border.h"
     25 #include "ui/views/controls/button/label_button.h"
     26 #include "ui/views/controls/link.h"
     27 #include "ui/views/controls/link_listener.h"
     28 #include "ui/views/layout/box_layout.h"
     29 #include "ui/views/layout/grid_layout.h"
     30 #include "ui/views/view.h"
     31 #include "ui/views/widget/widget.h"
     32 #include "url/gurl.h"
     33 
     34 #if defined(OS_WIN)
     35 #include "ui/base/l10n/l10n_util_win.h"
     36 #endif
     37 
     38 // FullscreenExitView ----------------------------------------------------------
     39 
     40 namespace {
     41 
     42 // Space between the site info label and the buttons / link.
     43 const int kMiddlePaddingPx = 30;
     44 
     45 class ButtonView : public views::View {
     46  public:
     47   ButtonView(views::ButtonListener* listener, int between_button_spacing);
     48   virtual ~ButtonView();
     49 
     50   // Returns an empty size when the view is not visible.
     51   virtual gfx::Size GetPreferredSize() OVERRIDE;
     52 
     53   views::LabelButton* accept_button() const { return accept_button_; }
     54   views::LabelButton* deny_button() const { return deny_button_; }
     55 
     56  private:
     57   views::LabelButton* accept_button_;
     58   views::LabelButton* deny_button_;
     59 
     60   DISALLOW_COPY_AND_ASSIGN(ButtonView);
     61 };
     62 
     63 ButtonView::ButtonView(views::ButtonListener* listener,
     64                        int between_button_spacing)
     65     : accept_button_(NULL),
     66       deny_button_(NULL) {
     67   accept_button_ = new views::LabelButton(listener, string16());
     68   accept_button_->SetStyle(views::Button::STYLE_NATIVE_TEXTBUTTON);
     69   accept_button_->set_focusable(false);
     70   AddChildView(accept_button_);
     71 
     72   deny_button_ = new views::LabelButton(listener, string16());
     73   deny_button_->SetStyle(views::Button::STYLE_NATIVE_TEXTBUTTON);
     74   deny_button_->set_focusable(false);
     75   AddChildView(deny_button_);
     76 
     77   SetLayoutManager(new views::BoxLayout(views::BoxLayout::kHorizontal, 0, 0,
     78                                         between_button_spacing));
     79 }
     80 
     81 ButtonView::~ButtonView() {
     82 }
     83 
     84 gfx::Size ButtonView::GetPreferredSize() {
     85   return visible() ? views::View::GetPreferredSize() : gfx::Size();
     86 }
     87 
     88 }  // namespace
     89 
     90 class FullscreenExitBubbleViews::FullscreenExitView
     91     : public views::View,
     92       public views::ButtonListener,
     93       public views::LinkListener {
     94  public:
     95   FullscreenExitView(FullscreenExitBubbleViews* bubble,
     96                      const string16& accelerator,
     97                      const GURL& url,
     98                      FullscreenExitBubbleType bubble_type);
     99   virtual ~FullscreenExitView();
    100 
    101   // views::ButtonListener
    102   virtual void ButtonPressed(views::Button* sender,
    103                              const ui::Event& event) OVERRIDE;
    104 
    105   // views::LinkListener
    106   virtual void LinkClicked(views::Link* source, int event_flags) OVERRIDE;
    107 
    108   void UpdateContent(const GURL& url, FullscreenExitBubbleType bubble_type);
    109 
    110  private:
    111   FullscreenExitBubbleViews* bubble_;
    112 
    113   // Clickable hint text for exiting fullscreen mode.
    114   views::Link* link_;
    115   // Instruction for exiting mouse lock.
    116   views::Label* mouse_lock_exit_instruction_;
    117   // Informational label: 'www.foo.com has gone fullscreen'.
    118   views::Label* message_label_;
    119   ButtonView* button_view_;
    120   const string16 browser_fullscreen_exit_accelerator_;
    121 
    122   DISALLOW_COPY_AND_ASSIGN(FullscreenExitView);
    123 };
    124 
    125 FullscreenExitBubbleViews::FullscreenExitView::FullscreenExitView(
    126     FullscreenExitBubbleViews* bubble,
    127     const string16& accelerator,
    128     const GURL& url,
    129     FullscreenExitBubbleType bubble_type)
    130     : bubble_(bubble),
    131       link_(NULL),
    132       mouse_lock_exit_instruction_(NULL),
    133       message_label_(NULL),
    134       button_view_(NULL),
    135       browser_fullscreen_exit_accelerator_(accelerator) {
    136   views::BubbleBorder* bubble_border = new views::BubbleBorder(
    137       views::BubbleBorder::NONE, views::BubbleBorder::SHADOW, SK_ColorWHITE);
    138   set_background(new views::BubbleBackground(bubble_border));
    139   set_border(bubble_border);
    140   set_focusable(false);
    141 
    142   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
    143   message_label_ = new views::Label();
    144   message_label_->SetFont(rb.GetFont(ui::ResourceBundle::MediumFont));
    145 
    146   mouse_lock_exit_instruction_ = new views::Label();
    147   mouse_lock_exit_instruction_->set_collapse_when_hidden(true);
    148   mouse_lock_exit_instruction_->SetText(bubble_->GetInstructionText());
    149   mouse_lock_exit_instruction_->SetFont(
    150       rb.GetFont(ui::ResourceBundle::MediumFont));
    151 
    152   link_ = new views::Link();
    153   link_->set_collapse_when_hidden(true);
    154   link_->set_focusable(false);
    155 #if defined(OS_CHROMEOS)
    156   // On CrOS, the link text doesn't change, since it doesn't show the shortcut.
    157   link_->SetText(l10n_util::GetStringUTF16(IDS_EXIT_FULLSCREEN_MODE));
    158 #endif
    159   link_->set_listener(this);
    160   link_->SetFont(rb.GetFont(ui::ResourceBundle::MediumFont));
    161   link_->SetPressedColor(message_label_->enabled_color());
    162   link_->SetEnabledColor(message_label_->enabled_color());
    163   link_->SetVisible(false);
    164 
    165   link_->SetBackgroundColor(background()->get_color());
    166   message_label_->SetBackgroundColor(background()->get_color());
    167   mouse_lock_exit_instruction_->SetBackgroundColor(background()->get_color());
    168 
    169   button_view_ = new ButtonView(this, kPaddingPx);
    170   button_view_->accept_button()->SetText(bubble->GetAllowButtonText());
    171 
    172   views::GridLayout* layout = new views::GridLayout(this);
    173   views::ColumnSet* columns = layout->AddColumnSet(0);
    174   columns->AddColumn(views::GridLayout::LEADING, views::GridLayout::CENTER, 0,
    175                      views::GridLayout::USE_PREF, 0, 0);
    176   columns->AddPaddingColumn(1, kMiddlePaddingPx);
    177   columns->AddColumn(views::GridLayout::LEADING, views::GridLayout::CENTER, 0,
    178                      views::GridLayout::USE_PREF, 0, 0);
    179   columns->AddColumn(views::GridLayout::LEADING, views::GridLayout::CENTER, 0,
    180                      views::GridLayout::USE_PREF, 0, 0);
    181   columns->AddColumn(views::GridLayout::LEADING, views::GridLayout::CENTER, 0,
    182                      views::GridLayout::USE_PREF, 0, 0);
    183 
    184   layout->StartRow(0, 0);
    185   layout->AddView(message_label_);
    186   layout->AddView(button_view_);
    187   layout->AddView(mouse_lock_exit_instruction_);
    188   layout->AddView(link_);
    189 
    190   gfx::Insets padding(kPaddingPx, kPaddingPx, kPaddingPx, kPaddingPx);
    191   padding += GetInsets();
    192   layout->SetInsets(padding);
    193   SetLayoutManager(layout);
    194 
    195   UpdateContent(url, bubble_type);
    196 }
    197 
    198 FullscreenExitBubbleViews::FullscreenExitView::~FullscreenExitView() {
    199 }
    200 
    201 void FullscreenExitBubbleViews::FullscreenExitView::ButtonPressed(
    202     views::Button* sender,
    203     const ui::Event& event) {
    204   if (sender == button_view_->accept_button())
    205     bubble_->Accept();
    206   else
    207     bubble_->Cancel();
    208 }
    209 
    210 void FullscreenExitBubbleViews::FullscreenExitView::LinkClicked(
    211     views::Link* link,
    212     int event_flags) {
    213   bubble_->ToggleFullscreen();
    214 }
    215 
    216 void FullscreenExitBubbleViews::FullscreenExitView::UpdateContent(
    217     const GURL& url,
    218     FullscreenExitBubbleType bubble_type) {
    219   DCHECK_NE(FEB_TYPE_NONE, bubble_type);
    220 
    221   message_label_->SetText(bubble_->GetCurrentMessageText());
    222   if (fullscreen_bubble::ShowButtonsForType(bubble_type)) {
    223     link_->SetVisible(false);
    224     mouse_lock_exit_instruction_->SetVisible(false);
    225     button_view_->SetVisible(true);
    226     button_view_->deny_button()->SetText(bubble_->GetCurrentDenyButtonText());
    227     button_view_->deny_button()->set_min_size(gfx::Size());
    228   } else {
    229     bool link_visible = true;
    230     string16 accelerator;
    231     if (bubble_type == FEB_TYPE_BROWSER_FULLSCREEN_EXIT_INSTRUCTION ||
    232         bubble_type == FEB_TYPE_BROWSER_EXTENSION_FULLSCREEN_EXIT_INSTRUCTION) {
    233       accelerator = browser_fullscreen_exit_accelerator_;
    234     } else if (bubble_type == FEB_TYPE_FULLSCREEN_EXIT_INSTRUCTION) {
    235       accelerator = l10n_util::GetStringUTF16(IDS_APP_ESC_KEY);
    236     } else {
    237       link_visible = false;
    238     }
    239 #if !defined(OS_CHROMEOS)
    240     if (link_visible) {
    241       link_->SetText(
    242           l10n_util::GetStringUTF16(IDS_EXIT_FULLSCREEN_MODE) +
    243           UTF8ToUTF16(" ") +
    244           l10n_util::GetStringFUTF16(IDS_EXIT_FULLSCREEN_MODE_ACCELERATOR,
    245               accelerator));
    246     }
    247 #endif
    248     link_->SetVisible(link_visible);
    249     mouse_lock_exit_instruction_->SetVisible(!link_visible);
    250     button_view_->SetVisible(false);
    251   }
    252 }
    253 
    254 
    255 // FullscreenExitBubbleViews ---------------------------------------------------
    256 
    257 FullscreenExitBubbleViews::FullscreenExitBubbleViews(
    258     BrowserView* browser_view,
    259     const GURL& url,
    260     FullscreenExitBubbleType bubble_type)
    261     : FullscreenExitBubble(browser_view->browser(), url, bubble_type),
    262       browser_view_(browser_view),
    263       popup_(NULL),
    264       animation_(new ui::SlideAnimation(this)),
    265       animated_attribute_(ANIMATED_ATTRIBUTE_BOUNDS) {
    266   animation_->Reset(1);
    267 
    268   // Create the contents view.
    269   ui::Accelerator accelerator(ui::VKEY_UNKNOWN, ui::EF_NONE);
    270   bool got_accelerator = browser_view_->GetWidget()->GetAccelerator(
    271       IDC_FULLSCREEN, &accelerator);
    272   DCHECK(got_accelerator);
    273   view_ = new FullscreenExitView(
    274       this, accelerator.GetShortcutText(), url, bubble_type_);
    275 
    276   // TODO(yzshen): Change to use the new views bubble, BubbleDelegateView.
    277   // TODO(pkotwicz): When this becomes a views bubble, make sure that this
    278   // bubble is ignored by ImmersiveModeControllerAsh::BubbleManager.
    279   // Initialize the popup.
    280   popup_ = new views::Widget;
    281   views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP);
    282   params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW;
    283   params.can_activate = false;
    284   params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
    285   params.parent = browser_view_->GetWidget()->GetNativeView();
    286   params.bounds = GetPopupRect(false);
    287   popup_->Init(params);
    288   gfx::Size size = GetPopupRect(true).size();
    289   popup_->SetContentsView(view_);
    290   // We set layout manager to NULL to prevent the widget from sizing its
    291   // contents to the same size as itself. This prevents the widget contents from
    292   // shrinking while we animate the height of the popup to give the impression
    293   // that it is sliding off the top of the screen.
    294   popup_->GetRootView()->SetLayoutManager(NULL);
    295   view_->SetBounds(0, 0, size.width(), size.height());
    296   popup_->Show();  // This does not activate the popup.
    297 
    298   popup_->AddObserver(this);
    299 
    300   registrar_.Add(
    301       this,
    302       chrome::NOTIFICATION_FULLSCREEN_CHANGED,
    303       content::Source<FullscreenController>(
    304           browser_view_->browser()->fullscreen_controller()));
    305 
    306   UpdateForImmersiveState();
    307 }
    308 
    309 FullscreenExitBubbleViews::~FullscreenExitBubbleViews() {
    310   popup_->RemoveObserver(this);
    311 
    312   // This is tricky.  We may be in an ATL message handler stack, in which case
    313   // the popup cannot be deleted yet.  We also can't set the popup's ownership
    314   // model to NATIVE_WIDGET_OWNS_WIDGET because if the user closed the last tab
    315   // while in fullscreen mode, Windows has already destroyed the popup HWND by
    316   // the time we get here, and thus either the popup will already have been
    317   // deleted (if we set this in our constructor) or the popup will never get
    318   // another OnFinalMessage() call (if not, as currently).  So instead, we tell
    319   // the popup to synchronously hide, and then asynchronously close and delete
    320   // itself.
    321   popup_->Close();
    322   base::MessageLoop::current()->DeleteSoon(FROM_HERE, popup_);
    323 }
    324 
    325 void FullscreenExitBubbleViews::UpdateContent(
    326     const GURL& url,
    327     FullscreenExitBubbleType bubble_type) {
    328   DCHECK_NE(FEB_TYPE_NONE, bubble_type);
    329   if (bubble_type_ == bubble_type && url_ == url)
    330     return;
    331 
    332   url_ = url;
    333   bubble_type_ = bubble_type;
    334   view_->UpdateContent(url_, bubble_type_);
    335 
    336   gfx::Size size = GetPopupRect(true).size();
    337   view_->SetSize(size);
    338   popup_->SetBounds(GetPopupRect(false));
    339   Show();
    340 
    341   // Stop watching the mouse even if UpdateMouseWatcher() will start watching
    342   // it again so that the popup with the new content is visible for at least
    343   // |kInitialDelayMs|.
    344   StopWatchingMouse();
    345 
    346   UpdateMouseWatcher();
    347 }
    348 
    349 void FullscreenExitBubbleViews::RepositionIfVisible() {
    350   if (popup_->IsVisible())
    351     UpdateBounds();
    352 }
    353 
    354 void FullscreenExitBubbleViews::UpdateMouseWatcher() {
    355   bool should_watch_mouse = false;
    356   if (popup_->IsVisible())
    357     should_watch_mouse = !fullscreen_bubble::ShowButtonsForType(bubble_type_);
    358   else
    359     should_watch_mouse = CanMouseTriggerSlideIn();
    360 
    361   if (should_watch_mouse == IsWatchingMouse())
    362     return;
    363 
    364   if (should_watch_mouse)
    365     StartWatchingMouse();
    366   else
    367     StopWatchingMouse();
    368 }
    369 
    370 void FullscreenExitBubbleViews::UpdateForImmersiveState() {
    371   AnimatedAttribute expected_animated_attribute =
    372       browser_view_->immersive_mode_controller()->IsEnabled() ?
    373           ANIMATED_ATTRIBUTE_OPACITY : ANIMATED_ATTRIBUTE_BOUNDS;
    374   if (animated_attribute_ != expected_animated_attribute) {
    375     // If an animation is currently in progress, skip to the end because
    376     // switching the animated attribute midway through the animation looks
    377     // weird.
    378     animation_->End();
    379 
    380     animated_attribute_ = expected_animated_attribute;
    381 
    382     // We may have finished hiding |popup_|. However, the bounds animation
    383     // assumes |popup_| has the opacity when it is fully shown and the opacity
    384     // animation assumes |popup_| has the bounds when |popup_| is fully shown.
    385     if (animated_attribute_ == ANIMATED_ATTRIBUTE_BOUNDS)
    386       popup_->SetOpacity(255);
    387     else
    388       UpdateBounds();
    389   }
    390 
    391   UpdateMouseWatcher();
    392 }
    393 
    394 void FullscreenExitBubbleViews::UpdateBounds() {
    395   gfx::Rect popup_rect(GetPopupRect(false));
    396   if (!popup_rect.IsEmpty()) {
    397     popup_->SetBounds(popup_rect);
    398     view_->SetY(popup_rect.height() - view_->height());
    399   }
    400 }
    401 
    402 views::View* FullscreenExitBubbleViews::GetBrowserRootView() const {
    403   return browser_view_->GetWidget()->GetRootView();
    404 }
    405 
    406 void FullscreenExitBubbleViews::AnimationProgressed(
    407     const ui::Animation* animation) {
    408   if (animated_attribute_ == ANIMATED_ATTRIBUTE_OPACITY) {
    409     int opacity = animation_->CurrentValueBetween(0, 255);
    410     if (opacity == 0) {
    411       popup_->Hide();
    412     } else {
    413       popup_->Show();
    414       popup_->SetOpacity(opacity);
    415     }
    416   } else {
    417     if (GetPopupRect(false).IsEmpty()) {
    418       popup_->Hide();
    419     } else {
    420       UpdateBounds();
    421       popup_->Show();
    422     }
    423   }
    424 }
    425 
    426 void FullscreenExitBubbleViews::AnimationEnded(
    427     const ui::Animation* animation) {
    428   AnimationProgressed(animation);
    429 }
    430 
    431 gfx::Rect FullscreenExitBubbleViews::GetPopupRect(
    432     bool ignore_animation_state) const {
    433   gfx::Size size(view_->GetPreferredSize());
    434   // NOTE: don't use the bounds of the root_view_. On linux GTK changing window
    435   // size is async. Instead we use the size of the screen.
    436   gfx::Screen* screen =
    437       gfx::Screen::GetScreenFor(browser_view_->GetWidget()->GetNativeView());
    438   gfx::Rect screen_bounds = screen->GetDisplayNearestWindow(
    439       browser_view_->GetWidget()->GetNativeView()).bounds();
    440   int x = screen_bounds.x() + (screen_bounds.width() - size.width()) / 2;
    441 
    442   int top_container_bottom = screen_bounds.y();
    443   if (browser_view_->immersive_mode_controller()->IsEnabled()) {
    444     // Skip querying the top container height in non-immersive fullscreen
    445     // because:
    446     // - The top container height is always zero in non-immersive fullscreen.
    447     // - Querying the top container height may return the height before entering
    448     //   fullscreen because layout is disabled while entering fullscreen.
    449     // A visual glitch due to the delayed layout is avoided in immersive
    450     // fullscreen because entering fullscreen starts with the top container
    451     // revealed. When revealed, the top container has the same height as before
    452     // entering fullscreen.
    453     top_container_bottom =
    454         browser_view_->top_container()->GetBoundsInScreen().bottom();
    455   }
    456   int y = top_container_bottom + kPopupTopPx;
    457 
    458   if (!ignore_animation_state &&
    459       animated_attribute_ == ANIMATED_ATTRIBUTE_BOUNDS) {
    460     int total_height = size.height() + kPopupTopPx;
    461     int popup_bottom = animation_->CurrentValueBetween(total_height, 0);
    462     int y_offset = std::min(popup_bottom, kPopupTopPx);
    463     size.set_height(size.height() - popup_bottom + y_offset);
    464     y -= y_offset;
    465   }
    466   return gfx::Rect(gfx::Point(x, y), size);
    467 }
    468 
    469 gfx::Point FullscreenExitBubbleViews::GetCursorScreenPoint() {
    470   gfx::Point cursor_pos = gfx::Screen::GetScreenFor(
    471       browser_view_->GetWidget()->GetNativeView())->GetCursorScreenPoint();
    472   views::View::ConvertPointToTarget(NULL, GetBrowserRootView(), &cursor_pos);
    473   return cursor_pos;
    474 }
    475 
    476 bool FullscreenExitBubbleViews::WindowContainsPoint(gfx::Point pos) {
    477   return GetBrowserRootView()->HitTestPoint(pos);
    478 }
    479 
    480 bool FullscreenExitBubbleViews::IsWindowActive() {
    481   return browser_view_->GetWidget()->IsActive();
    482 }
    483 
    484 void FullscreenExitBubbleViews::Hide() {
    485   animation_->SetSlideDuration(kSlideOutDurationMs);
    486   animation_->Hide();
    487 }
    488 
    489 void FullscreenExitBubbleViews::Show() {
    490   animation_->SetSlideDuration(kSlideInDurationMs);
    491   animation_->Show();
    492 }
    493 
    494 bool FullscreenExitBubbleViews::IsAnimating() {
    495   return animation_->is_animating();
    496 }
    497 
    498 bool FullscreenExitBubbleViews::CanMouseTriggerSlideIn() const {
    499   return !browser_view_->immersive_mode_controller()->IsEnabled();
    500 }
    501 
    502 void FullscreenExitBubbleViews::Observe(
    503     int type,
    504     const content::NotificationSource& source,
    505     const content::NotificationDetails& details) {
    506   DCHECK_EQ(chrome::NOTIFICATION_FULLSCREEN_CHANGED, type);
    507   UpdateForImmersiveState();
    508 }
    509 
    510 void FullscreenExitBubbleViews::OnWidgetVisibilityChanged(
    511     views::Widget* widget,
    512     bool visible) {
    513   UpdateMouseWatcher();
    514 }
    515