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/gtk/zoom_bubble_gtk.h"
6
7 #include "base/i18n/rtl.h"
8 #include "base/prefs/pref_service.h"
9 #include "base/strings/utf_string_conversions.h"
10 #include "chrome/browser/chrome_notification_types.h"
11 #include "chrome/browser/profiles/profile.h"
12 #include "chrome/browser/ui/browser.h"
13 #include "chrome/browser/ui/browser_finder.h"
14 #include "chrome/browser/ui/browser_window.h"
15 #include "chrome/browser/ui/gtk/gtk_theme_service.h"
16 #include "chrome/browser/ui/gtk/location_bar_view_gtk.h"
17 #include "chrome/browser/ui/zoom/zoom_controller.h"
18 #include "chrome/common/pref_names.h"
19 #include "content/public/browser/notification_details.h"
20 #include "content/public/browser/notification_source.h"
21 #include "content/public/browser/render_view_host.h"
22 #include "content/public/browser/web_contents.h"
23 #include "grit/generated_resources.h"
24 #include "ui/base/gtk/gtk_hig_constants.h"
25 #include "ui/base/l10n/l10n_util.h"
26 #include "ui/gfx/rect.h"
27
28 namespace {
29
30 // Pointer to singleton object (NULL if no bubble is open).
31 ZoomBubbleGtk* g_bubble = NULL;
32
33 // Number of milliseconds the bubble should stay open for if it will auto-close.
34 const int kBubbleCloseDelay = 1500;
35
36 // Need to manually set anchor width and height to ensure that the bubble shows
37 // in the correct spot the first time it is displayed when no icon is present.
38 const int kBubbleAnchorWidth = 20;
39 const int kBubbleAnchorHeight = 25;
40
41 } // namespace
42
43 // static
ShowBubble(content::WebContents * web_contents,bool auto_close)44 void ZoomBubbleGtk::ShowBubble(content::WebContents* web_contents,
45 bool auto_close) {
46 Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
47 DCHECK(browser && browser->window() && browser->fullscreen_controller());
48
49 LocationBar* location_bar = browser->window()->GetLocationBar();
50 GtkWidget* anchor = browser->window()->IsFullscreen() ?
51 GTK_WIDGET(browser->window()->GetNativeWindow()) :
52 static_cast<LocationBarViewGtk*>(location_bar)->zoom_widget();
53
54 // If the bubble is already showing and its |auto_close_| value is equal to
55 // |auto_close|, the bubble can be reused and only the label text needs to
56 // be updated.
57 if (g_bubble &&
58 g_bubble->auto_close_ == auto_close &&
59 g_bubble->bubble_->anchor_widget() == anchor) {
60 g_bubble->Refresh();
61 } else {
62 // If the bubble is already showing but its |auto_close_| value is not equal
63 // to |auto_close|, the bubble's focus properties must change, so the
64 // current bubble must be closed and a new one created.
65 CloseBubble();
66 DCHECK(!g_bubble);
67
68 g_bubble = new ZoomBubbleGtk(anchor,
69 web_contents,
70 auto_close,
71 browser->fullscreen_controller());
72 }
73 }
74
75 // static
CloseBubble()76 void ZoomBubbleGtk::CloseBubble() {
77 if (g_bubble)
78 g_bubble->Close();
79 }
80
81 // static
IsShowing()82 bool ZoomBubbleGtk::IsShowing() {
83 return g_bubble != NULL;
84 }
85
ZoomBubbleGtk(GtkWidget * anchor,content::WebContents * web_contents,bool auto_close,FullscreenController * fullscreen_controller)86 ZoomBubbleGtk::ZoomBubbleGtk(GtkWidget* anchor,
87 content::WebContents* web_contents,
88 bool auto_close,
89 FullscreenController* fullscreen_controller)
90 : auto_close_(auto_close),
91 mouse_inside_(false),
92 web_contents_(web_contents) {
93 GtkThemeService* theme_service =
94 GtkThemeService::GetFrom(Profile::FromBrowserContext(
95 web_contents_->GetBrowserContext()));
96
97 event_box_ = gtk_event_box_new();
98 gtk_event_box_set_visible_window(GTK_EVENT_BOX(event_box_), FALSE);
99 GtkWidget* container = gtk_vbox_new(FALSE, 0);
100 gtk_container_add(GTK_CONTAINER(event_box_), container);
101
102 ZoomController* zoom_controller =
103 ZoomController::FromWebContents(web_contents_);
104 int zoom_percent = zoom_controller->zoom_percent();
105 std::string percentage_text = UTF16ToUTF8(l10n_util::GetStringFUTF16Int(
106 IDS_TOOLTIP_ZOOM, zoom_percent));
107 label_ = theme_service->BuildLabel(percentage_text, ui::kGdkBlack);
108 gtk_widget_modify_font(label_, pango_font_description_from_string("13"));
109 gtk_misc_set_padding(GTK_MISC(label_),
110 ui::kControlSpacing, ui::kControlSpacing);
111 gtk_box_pack_start(GTK_BOX(container), label_, FALSE, FALSE, 0);
112
113 GtkWidget* set_default_button = gtk_button_new_with_label(
114 l10n_util::GetStringUTF8(IDS_ZOOM_SET_DEFAULT).c_str());
115
116 GtkWidget* alignment = gtk_alignment_new(0, 0, 1, 1);
117 gtk_alignment_set_padding(GTK_ALIGNMENT(alignment),
118 0,
119 ui::kControlSpacing,
120 ui::kControlSpacing,
121 ui::kControlSpacing);
122 gtk_container_add(GTK_CONTAINER(alignment), set_default_button);
123
124 gtk_box_pack_start(GTK_BOX(container), alignment, FALSE, FALSE, 0);
125
126 g_signal_connect(set_default_button, "clicked",
127 G_CALLBACK(&OnSetDefaultLinkClickThunk), this);
128
129 gtk_container_set_focus_child(GTK_CONTAINER(container), NULL);
130
131 gfx::Rect rect = gfx::Rect(kBubbleAnchorWidth, kBubbleAnchorHeight);
132 BubbleGtk::FrameStyle frame_style = gtk_widget_is_toplevel(anchor) ?
133 BubbleGtk::FIXED_TOP_RIGHT : BubbleGtk::ANCHOR_TOP_MIDDLE;
134 int bubble_options = BubbleGtk::MATCH_SYSTEM_THEME | BubbleGtk::POPUP_WINDOW;
135 bubble_ = BubbleGtk::Show(anchor, &rect, event_box_, frame_style,
136 auto_close ? bubble_options : bubble_options | BubbleGtk::GRAB_INPUT,
137 theme_service, NULL);
138
139 if (!bubble_) {
140 NOTREACHED();
141 return;
142 }
143
144 g_signal_connect(event_box_, "destroy",
145 G_CALLBACK(&OnDestroyThunk), this);
146
147 if (auto_close_) {
148 // If this is an auto-closing bubble, listen to leave/enter to keep the
149 // bubble alive if the mouse stays anywhere inside the bubble.
150 g_signal_connect_after(event_box_, "enter-notify-event",
151 G_CALLBACK(&OnMouseEnterThunk), this);
152 g_signal_connect(event_box_, "leave-notify-event",
153 G_CALLBACK(&OnMouseLeaveThunk), this);
154
155 // This is required as a leave is fired when the mouse goes from inside the
156 // bubble's container to inside the set default button.
157 gtk_widget_add_events(set_default_button, GDK_ENTER_NOTIFY_MASK);
158 g_signal_connect_after(set_default_button, "enter-notify-event",
159 G_CALLBACK(&OnMouseEnterThunk), this);
160 g_signal_connect(set_default_button, "leave-notify-event",
161 G_CALLBACK(&OnMouseLeaveThunk), this);
162 }
163
164 registrar_.Add(this,
165 chrome::NOTIFICATION_FULLSCREEN_CHANGED,
166 content::Source<FullscreenController>(fullscreen_controller));
167
168 StartTimerIfNecessary();
169 }
170
~ZoomBubbleGtk()171 ZoomBubbleGtk::~ZoomBubbleGtk() {
172 DCHECK_EQ(g_bubble, this);
173 // Set singleton pointer to NULL.
174 g_bubble = NULL;
175 }
176
Refresh()177 void ZoomBubbleGtk::Refresh() {
178 ZoomController* zoom_controller =
179 ZoomController::FromWebContents(web_contents_);
180 int zoom_percent = zoom_controller->zoom_percent();
181 base::string16 text =
182 l10n_util::GetStringFUTF16Int(IDS_TOOLTIP_ZOOM, zoom_percent);
183 gtk_label_set_text(GTK_LABEL(g_bubble->label_), UTF16ToUTF8(text).c_str());
184 StartTimerIfNecessary();
185 }
186
StartTimerIfNecessary()187 void ZoomBubbleGtk::StartTimerIfNecessary() {
188 if (!auto_close_ || mouse_inside_)
189 return;
190
191 if (timer_.IsRunning()) {
192 timer_.Reset();
193 } else {
194 timer_.Start(
195 FROM_HERE,
196 base::TimeDelta::FromMilliseconds(kBubbleCloseDelay),
197 this,
198 &ZoomBubbleGtk::Close);
199 }
200 }
201
StopTimerIfNecessary()202 void ZoomBubbleGtk::StopTimerIfNecessary() {
203 if (timer_.IsRunning())
204 timer_.Stop();
205 }
206
Close()207 void ZoomBubbleGtk::Close() {
208 DCHECK(bubble_);
209 bubble_->Close();
210 }
211
OnDestroy(GtkWidget * widget)212 void ZoomBubbleGtk::OnDestroy(GtkWidget* widget) {
213 // Listen to the destroy signal and delete this instance when it is caught.
214 delete this;
215 }
216
OnSetDefaultLinkClick(GtkWidget * widget)217 void ZoomBubbleGtk::OnSetDefaultLinkClick(GtkWidget* widget) {
218 double default_zoom_level = Profile::FromBrowserContext(
219 web_contents_->GetBrowserContext())->GetPrefs()->GetDouble(
220 prefs::kDefaultZoomLevel);
221 web_contents_->SetZoomLevel(default_zoom_level);
222 }
223
OnMouseEnter(GtkWidget * widget,GdkEventCrossing * event)224 gboolean ZoomBubbleGtk::OnMouseEnter(GtkWidget* widget,
225 GdkEventCrossing* event) {
226 mouse_inside_ = true;
227 StopTimerIfNecessary();
228 return FALSE;
229 }
230
OnMouseLeave(GtkWidget * widget,GdkEventCrossing * event)231 gboolean ZoomBubbleGtk::OnMouseLeave(GtkWidget* widget,
232 GdkEventCrossing* event) {
233 mouse_inside_ = false;
234 StartTimerIfNecessary();
235 return FALSE;
236 }
237
Observe(int type,const content::NotificationSource & source,const content::NotificationDetails & details)238 void ZoomBubbleGtk::Observe(int type,
239 const content::NotificationSource& source,
240 const content::NotificationDetails& details) {
241 DCHECK_EQ(type, chrome::NOTIFICATION_FULLSCREEN_CHANGED);
242 CloseBubble();
243 }
244