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