zoom_bubble_view.cc revision 5f1c94371a64b3196d4be9466099bb892df9b88e
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/location_bar/zoom_bubble_view.h"
6
7#include "base/i18n/rtl.h"
8#include "base/strings/stringprintf.h"
9#include "chrome/browser/chrome_notification_types.h"
10#include "chrome/browser/chrome_page_zoom.h"
11#include "chrome/browser/ui/browser.h"
12#include "chrome/browser/ui/browser_finder.h"
13#include "chrome/browser/ui/browser_tabstrip.h"
14#include "chrome/browser/ui/browser_window.h"
15#include "chrome/browser/ui/views/frame/browser_view.h"
16#include "chrome/browser/ui/views/location_bar/location_bar_view.h"
17#include "chrome/browser/ui/views/location_bar/zoom_view.h"
18#include "chrome/browser/ui/zoom/zoom_controller.h"
19#include "chrome/common/extensions/api/extension_action/action_info.h"
20#include "content/public/browser/notification_source.h"
21#include "extensions/common/manifest_handlers/icons_handler.h"
22#include "grit/generated_resources.h"
23#include "grit/theme_resources.h"
24#include "ui/base/l10n/l10n_util.h"
25#include "ui/base/resource/resource_bundle.h"
26#include "ui/gfx/favicon_size.h"
27#include "ui/views/controls/button/image_button.h"
28#include "ui/views/controls/button/label_button.h"
29#include "ui/views/controls/separator.h"
30#include "ui/views/layout/grid_layout.h"
31#include "ui/views/layout/layout_constants.h"
32#include "ui/views/widget/widget.h"
33
34namespace {
35
36// The number of milliseconds the bubble should stay on the screen if it will
37// close automatically.
38const int kBubbleCloseDelay = 1500;
39
40// The bubble's padding from the screen edge, used in fullscreen.
41const int kFullscreenPaddingEnd = 20;
42
43}  // namespace
44
45// static
46ZoomBubbleView* ZoomBubbleView::zoom_bubble_ = NULL;
47
48// static
49void ZoomBubbleView::ShowBubble(content::WebContents* web_contents,
50                                bool auto_close) {
51  Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
52  DCHECK(browser && browser->window() && browser->fullscreen_controller());
53
54  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser);
55  bool is_fullscreen = browser_view->IsFullscreen();
56  bool anchor_to_view = !is_fullscreen ||
57      browser_view->immersive_mode_controller()->IsRevealed();
58  views::View* anchor_view = anchor_to_view ?
59      browser_view->GetLocationBarView()->zoom_view() : NULL;
60
61  // Find the extension that initiated the zoom change, if any.
62  ZoomController* zoom_controller =
63      ZoomController::FromWebContents(web_contents);
64  const extensions::Extension* extension = zoom_controller->last_extension();
65
66  // If the bubble is already showing in this window and the zoom change was not
67  // initiated by an extension, then the bubble can be reused and only the label
68  // text needs to be updated.
69  if (zoom_bubble_ &&
70      zoom_bubble_->GetAnchorView() == anchor_view &&
71      !extension) {
72    zoom_bubble_->Refresh();
73    return;
74  }
75
76  // If the bubble is already showing but in a different tab, the current
77  // bubble must be closed and a new one created.
78  CloseBubble();
79
80  zoom_bubble_ = new ZoomBubbleView(anchor_view,
81                                    web_contents,
82                                    auto_close,
83                                    browser_view->immersive_mode_controller(),
84                                    browser->fullscreen_controller());
85
86  // If the zoom change was initiated by an extension, capture the relevent
87  // information from it.
88  if (extension)
89    zoom_bubble_->SetExtensionInfo(extension);
90
91  // If we do not have an anchor view, parent the bubble to the content area.
92  if (!anchor_to_view)
93    zoom_bubble_->set_parent_window(web_contents->GetTopLevelNativeWindow());
94
95  views::BubbleDelegateView::CreateBubble(zoom_bubble_);
96
97  // Adjust for fullscreen after creation as it relies on the content size.
98  if (is_fullscreen)
99    zoom_bubble_->AdjustForFullscreen(browser_view->GetBoundsInScreen());
100
101  if (auto_close)
102    zoom_bubble_->GetWidget()->ShowInactive();
103  else
104    zoom_bubble_->GetWidget()->Show();
105}
106
107// static
108void ZoomBubbleView::CloseBubble() {
109  if (zoom_bubble_)
110    zoom_bubble_->Close();
111}
112
113// static
114bool ZoomBubbleView::IsShowing() {
115  // The bubble may be in the process of closing.
116  return zoom_bubble_ != NULL && zoom_bubble_->GetWidget()->IsVisible();
117}
118
119// static
120const ZoomBubbleView* ZoomBubbleView::GetZoomBubbleForTest() {
121  return zoom_bubble_;
122}
123
124ZoomBubbleView::ZoomBubbleView(
125    views::View* anchor_view,
126    content::WebContents* web_contents,
127    bool auto_close,
128    ImmersiveModeController* immersive_mode_controller,
129    FullscreenController* fullscreen_controller)
130    : BubbleDelegateView(anchor_view, anchor_view ?
131          views::BubbleBorder::TOP_RIGHT : views::BubbleBorder::NONE),
132      image_button_(NULL),
133      label_(NULL),
134      web_contents_(web_contents),
135      auto_close_(auto_close),
136      immersive_mode_controller_(immersive_mode_controller) {
137  // Compensate for built-in vertical padding in the anchor view's image.
138  set_anchor_view_insets(gfx::Insets(5, 0, 5, 0));
139  set_notify_enter_exit_on_child(true);
140
141  // Add observers to close the bubble if the fullscreen state or immersive
142  // fullscreen revealed state changes.
143  registrar_.Add(this,
144                 chrome::NOTIFICATION_FULLSCREEN_CHANGED,
145                 content::Source<FullscreenController>(fullscreen_controller));
146  immersive_mode_controller_->AddObserver(this);
147}
148
149ZoomBubbleView::~ZoomBubbleView() {
150  if (immersive_mode_controller_)
151    immersive_mode_controller_->RemoveObserver(this);
152}
153
154void ZoomBubbleView::AdjustForFullscreen(const gfx::Rect& screen_bounds) {
155  if (GetAnchorView())
156    return;
157
158  // TODO(dbeam): should RTL logic be done in views::BubbleDelegateView?
159  const size_t bubble_half_width = width() / 2;
160  const int x_pos = base::i18n::IsRTL() ?
161      screen_bounds.x() + bubble_half_width + kFullscreenPaddingEnd :
162      screen_bounds.right() - bubble_half_width - kFullscreenPaddingEnd;
163  SetAnchorRect(gfx::Rect(x_pos, screen_bounds.y(), 0, 0));
164}
165
166void ZoomBubbleView::Refresh() {
167  ZoomController* zoom_controller =
168      ZoomController::FromWebContents(web_contents_);
169  int zoom_percent = zoom_controller->GetZoomPercent();
170  label_->SetText(
171      l10n_util::GetStringFUTF16Int(IDS_TOOLTIP_ZOOM, zoom_percent));
172  StartTimerIfNecessary();
173}
174
175void ZoomBubbleView::Close() {
176  GetWidget()->Close();
177}
178
179void ZoomBubbleView::SetExtensionInfo(const extensions::Extension* extension) {
180  DCHECK(extension);
181  extension_info_.id = extension->id();
182  extension_info_.name = extension->name();
183
184  ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
185  const gfx::ImageSkia& default_extension_icon_image =
186      *rb.GetImageSkiaNamed(IDR_EXTENSIONS_FAVICON);
187  int icon_size = gfx::kFaviconSize;
188
189  // We give first preference to an icon from the extension's icon set that
190  // matches the size of the default. But not all extensions will declare an
191  // icon set, or may not have an icon of the default size (we don't want the
192  // bubble to display, for example, a very large icon). In that case, if there
193  // is a browser-action icon (size-19) this is an acceptable alternative.
194  const ExtensionIconSet& icons = extensions::IconsInfo::GetIcons(extension);
195  bool has_default_sized_icon =
196      !icons.Get(gfx::kFaviconSize, ExtensionIconSet::MATCH_EXACTLY).empty();
197  if (has_default_sized_icon) {
198    extension_info_.icon_image.reset(
199        new extensions::IconImage(web_contents_->GetBrowserContext(),
200                                  extension,
201                                  icons,
202                                  icon_size,
203                                  default_extension_icon_image,
204                                  this));
205    return;
206  }
207
208  const extensions::ActionInfo* browser_action =
209      extensions::ActionInfo::GetBrowserActionInfo(extension);
210  if (!browser_action || browser_action->default_icon.empty())
211    return;
212
213  icon_size = browser_action->default_icon.map().begin()->first;
214  extension_info_.icon_image.reset(
215      new extensions::IconImage(web_contents_->GetBrowserContext(),
216                                extension,
217                                browser_action->default_icon,
218                                icon_size,
219                                default_extension_icon_image,
220                                this));
221}
222
223void ZoomBubbleView::StartTimerIfNecessary() {
224  if (auto_close_) {
225    if (timer_.IsRunning()) {
226      timer_.Reset();
227    } else {
228      timer_.Start(
229          FROM_HERE,
230          base::TimeDelta::FromMilliseconds(kBubbleCloseDelay),
231          this,
232          &ZoomBubbleView::Close);
233    }
234  }
235}
236
237void ZoomBubbleView::StopTimer() {
238  timer_.Stop();
239}
240
241void ZoomBubbleView::OnExtensionIconImageChanged(
242    extensions::IconImage* /* image */) {
243  image_button_->SetImage(views::Button::STATE_NORMAL,
244                          &extension_info_.icon_image->image_skia());
245  image_button_->SchedulePaint();
246}
247
248void ZoomBubbleView::OnMouseEntered(const ui::MouseEvent& event) {
249  StopTimer();
250}
251
252void ZoomBubbleView::OnMouseExited(const ui::MouseEvent& event) {
253  StartTimerIfNecessary();
254}
255
256void ZoomBubbleView::OnGestureEvent(ui::GestureEvent* event) {
257  if (!zoom_bubble_ || !zoom_bubble_->auto_close_ ||
258      event->type() != ui::ET_GESTURE_TAP) {
259    return;
260  }
261
262  auto_close_ = false;
263  StopTimer();
264  event->SetHandled();
265}
266
267void ZoomBubbleView::ButtonPressed(views::Button* sender,
268                                   const ui::Event& event) {
269  if (sender == image_button_) {
270    DCHECK(extension_info_.icon_image) << "Invalid button press.";
271    Browser* browser = chrome::FindBrowserWithWebContents(web_contents_);
272    chrome::AddSelectedTabWithURL(
273        browser,
274        GURL(base::StringPrintf("chrome://extensions?id=%s",
275                                extension_info_.id.c_str())),
276        content::PAGE_TRANSITION_FROM_API);
277  } else {
278    chrome_page_zoom::Zoom(web_contents_, content::PAGE_ZOOM_RESET);
279  }
280}
281
282void ZoomBubbleView::Init() {
283  // Set up the layout of the zoom bubble. A grid layout is used because
284  // sometimes an extension icon is shown next to the zoom label.
285  views::GridLayout* grid_layout = new views::GridLayout(this);
286  SetLayoutManager(grid_layout);
287  views::ColumnSet* columns = grid_layout->AddColumnSet(0);
288  // First row.
289  if (extension_info_.icon_image) {
290    columns->AddColumn(views::GridLayout::CENTER,views::GridLayout::CENTER, 2,
291                       views::GridLayout::USE_PREF, 0, 0);
292  }
293  columns->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 1,
294                     views::GridLayout::USE_PREF, 0, 0);
295  grid_layout->StartRow(0, 0);
296
297  // If this zoom change was initiated by an extension, that extension will be
298  // attributed by showing its icon in the zoom bubble.
299  if (extension_info_.icon_image) {
300    image_button_ = new views::ImageButton(this);
301    image_button_->SetTooltipText(l10n_util::GetStringFUTF16(
302        IDS_TOOLTIP_ZOOM_EXTENSION_ICON,
303        base::UTF8ToUTF16(extension_info_.name)));
304    image_button_->SetImage(views::Button::STATE_NORMAL,
305                            &extension_info_.icon_image->image_skia());
306    grid_layout->AddView(image_button_);
307  }
308
309  // Add zoom label with the new zoom percent.
310  ZoomController* zoom_controller =
311      ZoomController::FromWebContents(web_contents_);
312  int zoom_percent = zoom_controller->GetZoomPercent();
313  label_ = new views::Label(
314      l10n_util::GetStringFUTF16Int(IDS_TOOLTIP_ZOOM, zoom_percent));
315  label_->SetFontList(
316      ui::ResourceBundle::GetSharedInstance().GetFontList(
317          ui::ResourceBundle::MediumFont));
318  grid_layout->AddView(label_);
319
320  // Second row.
321  grid_layout->AddPaddingRow(0, 8);
322  columns = grid_layout->AddColumnSet(1);
323  columns->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 1,
324                     views::GridLayout::USE_PREF, 0, 0);
325  grid_layout->StartRow(0, 1);
326
327  // Add "Reset to Default" button.
328  views::LabelButton* set_default_button = new views::LabelButton(
329      this, l10n_util::GetStringUTF16(IDS_ZOOM_SET_DEFAULT));
330  set_default_button->SetStyle(views::Button::STYLE_BUTTON);
331  set_default_button->SetHorizontalAlignment(gfx::ALIGN_CENTER);
332  grid_layout->AddView(set_default_button);
333
334  StartTimerIfNecessary();
335}
336
337void ZoomBubbleView::Observe(int type,
338                             const content::NotificationSource& source,
339                             const content::NotificationDetails& details) {
340  DCHECK_EQ(type, chrome::NOTIFICATION_FULLSCREEN_CHANGED);
341  CloseBubble();
342}
343
344void ZoomBubbleView::OnImmersiveRevealStarted() {
345  CloseBubble();
346}
347
348void ZoomBubbleView::OnImmersiveModeControllerDestroyed() {
349  immersive_mode_controller_ = NULL;
350}
351
352void ZoomBubbleView::WindowClosing() {
353  // |zoom_bubble_| can be a new bubble by this point (as Close(); doesn't
354  // call this right away). Only set to NULL when it's this bubble.
355  if (zoom_bubble_ == this)
356    zoom_bubble_ = NULL;
357}
358
359ZoomBubbleView::ZoomBubbleExtensionInfo::ZoomBubbleExtensionInfo() {}
360
361ZoomBubbleView::ZoomBubbleExtensionInfo::~ZoomBubbleExtensionInfo() {}
362