OLD | NEW |
| (Empty) |
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/notifications/balloon_view_gtk.h" | |
6 | |
7 #include <gtk/gtk.h> | |
8 | |
9 #include <string> | |
10 #include <vector> | |
11 | |
12 #include "base/bind.h" | |
13 #include "base/debug/trace_event.h" | |
14 #include "base/message_loop/message_loop.h" | |
15 #include "base/strings/string_util.h" | |
16 #include "chrome/browser/chrome_notification_types.h" | |
17 #include "chrome/browser/notifications/balloon.h" | |
18 #include "chrome/browser/notifications/desktop_notification_service.h" | |
19 #include "chrome/browser/notifications/notification.h" | |
20 #include "chrome/browser/notifications/notification_options_menu_model.h" | |
21 #include "chrome/browser/profiles/profile.h" | |
22 #include "chrome/browser/themes/theme_service.h" | |
23 #include "chrome/browser/ui/browser_list.h" | |
24 #include "chrome/browser/ui/browser_window.h" | |
25 #include "chrome/browser/ui/gtk/custom_button.h" | |
26 #include "chrome/browser/ui/gtk/gtk_theme_service.h" | |
27 #include "chrome/browser/ui/gtk/gtk_util.h" | |
28 #include "chrome/browser/ui/gtk/menu_gtk.h" | |
29 #include "chrome/browser/ui/gtk/notifications/balloon_view_host_gtk.h" | |
30 #include "chrome/browser/ui/gtk/rounded_window.h" | |
31 #include "content/public/browser/notification_source.h" | |
32 #include "content/public/browser/render_view_host.h" | |
33 #include "content/public/browser/render_widget_host_view.h" | |
34 #include "content/public/browser/web_contents.h" | |
35 #include "extensions/browser/extension_host.h" | |
36 #include "extensions/browser/process_manager.h" | |
37 #include "extensions/common/extension.h" | |
38 #include "grit/generated_resources.h" | |
39 #include "grit/theme_resources.h" | |
40 #include "ui/base/gtk/gtk_hig_constants.h" | |
41 #include "ui/base/l10n/l10n_util.h" | |
42 #include "ui/base/resource/resource_bundle.h" | |
43 #include "ui/gfx/animation/slide_animation.h" | |
44 #include "ui/gfx/canvas.h" | |
45 #include "ui/gfx/insets.h" | |
46 #include "ui/gfx/native_widget_types.h" | |
47 | |
48 namespace { | |
49 | |
50 // Margin, in pixels, between the notification frame and the contents | |
51 // of the notification. | |
52 const int kTopMargin = 0; | |
53 const int kBottomMargin = 1; | |
54 const int kLeftMargin = 1; | |
55 const int kRightMargin = 1; | |
56 | |
57 // Properties of the origin label. | |
58 const int kLeftLabelMargin = 8; | |
59 | |
60 // TODO(johnnyg): Add a shadow for the frame. | |
61 const int kLeftShadowWidth = 0; | |
62 const int kRightShadowWidth = 0; | |
63 const int kTopShadowWidth = 0; | |
64 const int kBottomShadowWidth = 0; | |
65 | |
66 // Space in pixels between text and icon on the buttons. | |
67 const int kButtonSpacing = 3; | |
68 | |
69 // Number of characters to show in the origin label before ellipsis. | |
70 const int kOriginLabelCharacters = 18; | |
71 | |
72 // The shelf height for the system default font size. It is scaled | |
73 // with changes in the default font size. | |
74 const int kDefaultShelfHeight = 25; | |
75 | |
76 // The amount that the bubble collections class offsets from the side of the | |
77 // screen. | |
78 const int kScreenBorder = 5; | |
79 | |
80 // Colors specified in various ways for different parts of the UI. | |
81 // These match the windows colors in balloon_view.cc | |
82 const char* kLabelColor = "#7D7D7D"; | |
83 const double kShelfBackgroundColorR = 245.0 / 255.0; | |
84 const double kShelfBackgroundColorG = 245.0 / 255.0; | |
85 const double kShelfBackgroundColorB = 245.0 / 255.0; | |
86 const double kDividerLineColorR = 180.0 / 255.0; | |
87 const double kDividerLineColorG = 180.0 / 255.0; | |
88 const double kDividerLineColorB = 180.0 / 255.0; | |
89 | |
90 // Makes the website label relatively smaller to the base text size. | |
91 const char* kLabelMarkup = "<span size=\"small\" color=\"%s\">%s</span>"; | |
92 | |
93 } // namespace | |
94 | |
95 BalloonViewImpl::BalloonViewImpl(BalloonCollection* collection) | |
96 : balloon_(NULL), | |
97 theme_service_(NULL), | |
98 frame_container_(NULL), | |
99 shelf_(NULL), | |
100 hbox_(NULL), | |
101 html_container_(NULL), | |
102 menu_showing_(false), | |
103 pending_close_(false), | |
104 weak_factory_(this) {} | |
105 | |
106 BalloonViewImpl::~BalloonViewImpl() { | |
107 if (frame_container_) { | |
108 GtkWidget* widget = frame_container_; | |
109 frame_container_ = NULL; | |
110 gtk_widget_hide(widget); | |
111 } | |
112 } | |
113 | |
114 void BalloonViewImpl::Close(bool by_user) { | |
115 // Delay a system-initiated close if the menu is showing. | |
116 if (!by_user && menu_showing_) { | |
117 pending_close_ = true; | |
118 } else { | |
119 base::MessageLoop::current()->PostTask( | |
120 FROM_HERE, | |
121 base::Bind(&BalloonViewImpl::DelayedClose, | |
122 weak_factory_.GetWeakPtr(), | |
123 by_user)); | |
124 } | |
125 } | |
126 | |
127 gfx::Size BalloonViewImpl::GetSize() const { | |
128 // BalloonView has no size if it hasn't been shown yet (which is when | |
129 // balloon_ is set). | |
130 if (!balloon_) | |
131 return gfx::Size(); | |
132 | |
133 // Although this may not be the instantaneous size of the balloon if | |
134 // called in the middle of an animation, it is the effective size that | |
135 // will result from the animation. | |
136 return gfx::Size(GetDesiredTotalWidth(), GetDesiredTotalHeight()); | |
137 } | |
138 | |
139 BalloonHost* BalloonViewImpl::GetHost() const { | |
140 return html_contents_.get(); | |
141 } | |
142 | |
143 void BalloonViewImpl::DelayedClose(bool by_user) { | |
144 html_contents_->Shutdown(); | |
145 if (frame_container_) { | |
146 // It's possible that |frame_container_| was destroyed before the | |
147 // BalloonViewImpl if our related browser window was closed first. | |
148 gtk_widget_hide(frame_container_); | |
149 } | |
150 balloon_->OnClose(by_user); | |
151 } | |
152 | |
153 void BalloonViewImpl::RepositionToBalloon() { | |
154 if (!frame_container_) { | |
155 // No need to create a slide animation when this balloon is fading out. | |
156 return; | |
157 } | |
158 | |
159 DCHECK(balloon_); | |
160 | |
161 // Create an amination from the current position to the desired one. | |
162 int start_x; | |
163 int start_y; | |
164 int start_w; | |
165 int start_h; | |
166 gtk_window_get_position(GTK_WINDOW(frame_container_), &start_x, &start_y); | |
167 gtk_window_get_size(GTK_WINDOW(frame_container_), &start_w, &start_h); | |
168 | |
169 int end_x = balloon_->GetPosition().x(); | |
170 int end_y = balloon_->GetPosition().y(); | |
171 int end_w = GetDesiredTotalWidth(); | |
172 int end_h = GetDesiredTotalHeight(); | |
173 | |
174 anim_frame_start_ = gfx::Rect(start_x, start_y, start_w, start_h); | |
175 anim_frame_end_ = gfx::Rect(end_x, end_y, end_w, end_h); | |
176 animation_.reset(new gfx::SlideAnimation(this)); | |
177 animation_->Show(); | |
178 } | |
179 | |
180 void BalloonViewImpl::AnimationProgressed(const gfx::Animation* animation) { | |
181 DCHECK_EQ(animation, animation_.get()); | |
182 | |
183 // Linear interpolation from start to end position. | |
184 double end = animation->GetCurrentValue(); | |
185 double start = 1.0 - end; | |
186 | |
187 gfx::Rect frame_position( | |
188 static_cast<int>(start * anim_frame_start_.x() + | |
189 end * anim_frame_end_.x()), | |
190 static_cast<int>(start * anim_frame_start_.y() + | |
191 end * anim_frame_end_.y()), | |
192 static_cast<int>(start * anim_frame_start_.width() + | |
193 end * anim_frame_end_.width()), | |
194 static_cast<int>(start * anim_frame_start_.height() + | |
195 end * anim_frame_end_.height())); | |
196 gtk_window_resize(GTK_WINDOW(frame_container_), | |
197 frame_position.width(), frame_position.height()); | |
198 gtk_window_move(GTK_WINDOW(frame_container_), | |
199 frame_position.x(), frame_position.y()); | |
200 | |
201 gfx::Rect contents_rect = GetContentsRectangle(); | |
202 html_contents_->UpdateActualSize(contents_rect.size()); | |
203 } | |
204 | |
205 void BalloonViewImpl::Show(Balloon* balloon) { | |
206 theme_service_ = GtkThemeService::GetFrom(balloon->profile()); | |
207 | |
208 const std::string source_label_text = l10n_util::GetStringFUTF8( | |
209 IDS_NOTIFICATION_BALLOON_SOURCE_LABEL, | |
210 balloon->notification().display_source()); | |
211 const std::string options_text = | |
212 l10n_util::GetStringUTF8(IDS_NOTIFICATION_OPTIONS_MENU_LABEL); | |
213 const std::string dismiss_text = | |
214 l10n_util::GetStringUTF8(IDS_NOTIFICATION_BALLOON_DISMISS_LABEL); | |
215 | |
216 balloon_ = balloon; | |
217 frame_container_ = gtk_window_new(GTK_WINDOW_POPUP); | |
218 | |
219 g_signal_connect(frame_container_, "expose-event", | |
220 G_CALLBACK(OnExposeThunk), this); | |
221 g_signal_connect(frame_container_, "destroy", | |
222 G_CALLBACK(OnDestroyThunk), this); | |
223 | |
224 // Construct the options menu. | |
225 options_menu_model_.reset(new NotificationOptionsMenuModel(balloon_)); | |
226 options_menu_.reset(new MenuGtk(this, options_menu_model_.get())); | |
227 | |
228 // Create a BalloonViewHost to host the HTML contents of this balloon. | |
229 html_contents_.reset(new BalloonViewHost(balloon)); | |
230 html_contents_->Init(); | |
231 gfx::NativeView contents = html_contents_->native_view(); | |
232 g_signal_connect_after(contents, "expose-event", | |
233 G_CALLBACK(OnContentsExposeThunk), this); | |
234 | |
235 // Divide the frame vertically into the shelf and the content area. | |
236 GtkWidget* vbox = gtk_vbox_new(0, 0); | |
237 gtk_container_add(GTK_CONTAINER(frame_container_), vbox); | |
238 | |
239 // Create the toolbar. | |
240 shelf_ = gtk_hbox_new(FALSE, 0); | |
241 gtk_widget_set_size_request(GTK_WIDGET(shelf_), -1, GetShelfHeight()); | |
242 gtk_container_add(GTK_CONTAINER(vbox), shelf_); | |
243 | |
244 // Create a label for the source of the notification and add it to the | |
245 // toolbar. | |
246 GtkWidget* source_label_ = gtk_label_new(NULL); | |
247 char* markup = g_markup_printf_escaped(kLabelMarkup, | |
248 kLabelColor, | |
249 source_label_text.c_str()); | |
250 gtk_label_set_markup(GTK_LABEL(source_label_), markup); | |
251 g_free(markup); | |
252 gtk_label_set_max_width_chars(GTK_LABEL(source_label_), | |
253 kOriginLabelCharacters); | |
254 gtk_label_set_ellipsize(GTK_LABEL(source_label_), PANGO_ELLIPSIZE_END); | |
255 GtkWidget* label_alignment = gtk_alignment_new(0, 0.5, 0, 0); | |
256 gtk_alignment_set_padding(GTK_ALIGNMENT(label_alignment), | |
257 0, 0, kLeftLabelMargin, 0); | |
258 gtk_container_add(GTK_CONTAINER(label_alignment), source_label_); | |
259 gtk_box_pack_start(GTK_BOX(shelf_), label_alignment, FALSE, FALSE, 0); | |
260 | |
261 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); | |
262 | |
263 // Create a button to dismiss the balloon and add it to the toolbar. | |
264 close_button_.reset(CustomDrawButton::CloseButtonBar(theme_service_)); | |
265 close_button_->SetBackground( | |
266 SK_ColorBLACK, | |
267 rb.GetImageNamed(IDR_CLOSE_1).AsBitmap(), | |
268 rb.GetImageNamed(IDR_CLOSE_1_MASK).AsBitmap()); | |
269 gtk_widget_set_tooltip_text(close_button_->widget(), dismiss_text.c_str()); | |
270 g_signal_connect(close_button_->widget(), "clicked", | |
271 G_CALLBACK(OnCloseButtonThunk), this); | |
272 gtk_widget_set_can_focus(close_button_->widget(), FALSE); | |
273 GtkWidget* close_alignment = gtk_alignment_new(0.0, 0.5, 0, 0); | |
274 gtk_container_add(GTK_CONTAINER(close_alignment), close_button_->widget()); | |
275 gtk_box_pack_end(GTK_BOX(shelf_), close_alignment, FALSE, FALSE, | |
276 kButtonSpacing); | |
277 | |
278 // Create a button for showing the options menu, and add it to the toolbar. | |
279 options_menu_button_.reset(new CustomDrawButton(IDR_BALLOON_WRENCH, | |
280 IDR_BALLOON_WRENCH_P, | |
281 IDR_BALLOON_WRENCH_H, | |
282 0)); | |
283 gtk_widget_set_tooltip_text(options_menu_button_->widget(), | |
284 options_text.c_str()); | |
285 g_signal_connect(options_menu_button_->widget(), "button-press-event", | |
286 G_CALLBACK(OnOptionsMenuButtonThunk), this); | |
287 gtk_widget_set_can_focus(options_menu_button_->widget(), FALSE); | |
288 GtkWidget* options_alignment = gtk_alignment_new(0.0, 0.5, 0, 0); | |
289 gtk_container_add(GTK_CONTAINER(options_alignment), | |
290 options_menu_button_->widget()); | |
291 gtk_box_pack_end(GTK_BOX(shelf_), options_alignment, FALSE, FALSE, 0); | |
292 | |
293 // Add main contents to bubble. | |
294 GtkWidget* alignment = gtk_alignment_new(0.0, 0.0, 1.0, 1.0); | |
295 gtk_alignment_set_padding( | |
296 GTK_ALIGNMENT(alignment), | |
297 kTopMargin, kBottomMargin, kLeftMargin, kRightMargin); | |
298 gtk_widget_show_all(alignment); | |
299 gtk_container_add(GTK_CONTAINER(alignment), contents); | |
300 gtk_container_add(GTK_CONTAINER(vbox), alignment); | |
301 gtk_widget_show_all(vbox); | |
302 | |
303 notification_registrar_.Add(this, chrome::NOTIFICATION_BROWSER_THEME_CHANGED, | |
304 content::Source<ThemeService>(theme_service_)); | |
305 | |
306 // We don't do InitThemesFor() because it just forces a redraw. | |
307 gtk_util::ActAsRoundedWindow(frame_container_, ui::kGdkBlack, 3, | |
308 gtk_util::ROUNDED_ALL, | |
309 gtk_util::BORDER_ALL); | |
310 | |
311 // Realize the frame container so we can do size calculations. | |
312 gtk_widget_realize(frame_container_); | |
313 | |
314 // Update to make sure we have everything sized properly and then move our | |
315 // window offscreen for its initial animation. | |
316 html_contents_->UpdateActualSize(balloon_->content_size()); | |
317 int window_width; | |
318 gtk_window_get_size(GTK_WINDOW(frame_container_), &window_width, NULL); | |
319 | |
320 int pos_x = gdk_screen_width() - window_width - kScreenBorder; | |
321 int pos_y = gdk_screen_height(); | |
322 gtk_window_move(GTK_WINDOW(frame_container_), pos_x, pos_y); | |
323 balloon_->SetPosition(gfx::Point(pos_x, pos_y), false); | |
324 gtk_widget_show_all(frame_container_); | |
325 | |
326 notification_registrar_.Add(this, | |
327 chrome::NOTIFICATION_NOTIFY_BALLOON_DISCONNECTED, | |
328 content::Source<Balloon>(balloon)); | |
329 } | |
330 | |
331 void BalloonViewImpl::Update() { | |
332 DCHECK(html_contents_.get()) << "BalloonView::Update called before Show"; | |
333 if (!html_contents_->web_contents()) | |
334 return; | |
335 html_contents_->web_contents()->GetController().LoadURL( | |
336 balloon_->notification().content_url(), content::Referrer(), | |
337 content::PAGE_TRANSITION_LINK, std::string()); | |
338 } | |
339 | |
340 gfx::Point BalloonViewImpl::GetContentsOffset() const { | |
341 return gfx::Point(kLeftShadowWidth + kLeftMargin, | |
342 GetShelfHeight() + kTopShadowWidth + kTopMargin); | |
343 } | |
344 | |
345 int BalloonViewImpl::GetShelfHeight() const { | |
346 // TODO(johnnyg): add scaling here. | |
347 return kDefaultShelfHeight; | |
348 } | |
349 | |
350 int BalloonViewImpl::GetDesiredTotalWidth() const { | |
351 return balloon_->content_size().width() + | |
352 kLeftMargin + kRightMargin + kLeftShadowWidth + kRightShadowWidth; | |
353 } | |
354 | |
355 int BalloonViewImpl::GetDesiredTotalHeight() const { | |
356 return balloon_->content_size().height() + | |
357 kTopMargin + kBottomMargin + kTopShadowWidth + kBottomShadowWidth + | |
358 GetShelfHeight(); | |
359 } | |
360 | |
361 gfx::Rect BalloonViewImpl::GetContentsRectangle() const { | |
362 if (!frame_container_) | |
363 return gfx::Rect(); | |
364 | |
365 gfx::Size content_size = balloon_->content_size(); | |
366 gfx::Point offset = GetContentsOffset(); | |
367 int x = 0, y = 0; | |
368 gtk_window_get_position(GTK_WINDOW(frame_container_), &x, &y); | |
369 return gfx::Rect(x + offset.x(), y + offset.y(), | |
370 content_size.width(), content_size.height()); | |
371 } | |
372 | |
373 void BalloonViewImpl::Observe(int type, | |
374 const content::NotificationSource& source, | |
375 const content::NotificationDetails& details) { | |
376 if (type == chrome::NOTIFICATION_NOTIFY_BALLOON_DISCONNECTED) { | |
377 // If the renderer process attached to this balloon is disconnected | |
378 // (e.g., because of a crash), we want to close the balloon. | |
379 notification_registrar_.Remove(this, | |
380 chrome::NOTIFICATION_NOTIFY_BALLOON_DISCONNECTED, | |
381 content::Source<Balloon>(balloon_)); | |
382 Close(false); | |
383 } else if (type == chrome::NOTIFICATION_BROWSER_THEME_CHANGED) { | |
384 // Since all the buttons change their own properties, and our expose does | |
385 // all the real differences, we'll need a redraw. | |
386 gtk_widget_queue_draw(frame_container_); | |
387 } else { | |
388 NOTREACHED(); | |
389 } | |
390 } | |
391 | |
392 void BalloonViewImpl::OnCloseButton(GtkWidget* widget) { | |
393 Close(true); | |
394 } | |
395 | |
396 // We draw black dots on the bottom left and right corners to fill in the | |
397 // border. Otherwise, the border has a gap because the sharp corners of the | |
398 // HTML view cut off the roundedness of the notification window. | |
399 gboolean BalloonViewImpl::OnContentsExpose(GtkWidget* sender, | |
400 GdkEventExpose* event) { | |
401 TRACE_EVENT0("ui::gtk", "BalloonViewImpl::OnContentsExpose"); | |
402 cairo_t* cr = gdk_cairo_create(gtk_widget_get_window(sender)); | |
403 gdk_cairo_rectangle(cr, &event->area); | |
404 cairo_clip(cr); | |
405 | |
406 GtkAllocation allocation; | |
407 gtk_widget_get_allocation(sender, &allocation); | |
408 | |
409 // According to a discussion on a mailing list I found, these degenerate | |
410 // paths are the officially supported way to draw points in Cairo. | |
411 cairo_set_source_rgb(cr, 0, 0, 0); | |
412 cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); | |
413 cairo_set_line_width(cr, 1.0); | |
414 cairo_move_to(cr, 0.5, allocation.height - 0.5); | |
415 cairo_close_path(cr); | |
416 cairo_move_to(cr, allocation.width - 0.5, allocation.height - 0.5); | |
417 cairo_close_path(cr); | |
418 cairo_stroke(cr); | |
419 cairo_destroy(cr); | |
420 | |
421 return FALSE; | |
422 } | |
423 | |
424 gboolean BalloonViewImpl::OnExpose(GtkWidget* sender, GdkEventExpose* event) { | |
425 TRACE_EVENT0("ui::gtk", "BalloonViewImpl::OnExpose"); | |
426 cairo_t* cr = gdk_cairo_create(gtk_widget_get_window(sender)); | |
427 gdk_cairo_rectangle(cr, &event->area); | |
428 cairo_clip(cr); | |
429 | |
430 gfx::Size content_size = balloon_->content_size(); | |
431 gfx::Point offset = GetContentsOffset(); | |
432 | |
433 // Draw a background color behind the shelf. | |
434 cairo_set_source_rgb(cr, kShelfBackgroundColorR, | |
435 kShelfBackgroundColorG, kShelfBackgroundColorB); | |
436 cairo_rectangle(cr, kLeftMargin, kTopMargin + 0.5, | |
437 content_size.width() - 0.5, GetShelfHeight()); | |
438 cairo_fill(cr); | |
439 | |
440 // Now draw a one pixel line between content and shelf. | |
441 cairo_move_to(cr, offset.x(), offset.y() - 1); | |
442 cairo_line_to(cr, offset.x() + content_size.width(), offset.y() - 1); | |
443 cairo_set_line_width(cr, 0.5); | |
444 cairo_set_source_rgb(cr, kDividerLineColorR, | |
445 kDividerLineColorG, kDividerLineColorB); | |
446 cairo_stroke(cr); | |
447 | |
448 cairo_destroy(cr); | |
449 | |
450 return FALSE; | |
451 } | |
452 | |
453 void BalloonViewImpl::OnOptionsMenuButton(GtkWidget* widget, | |
454 GdkEventButton* event) { | |
455 menu_showing_ = true; | |
456 options_menu_->PopupForWidget(widget, event->button, event->time); | |
457 } | |
458 | |
459 // Called when the menu stops showing. | |
460 void BalloonViewImpl::StoppedShowing() { | |
461 menu_showing_ = false; | |
462 if (pending_close_) { | |
463 base::MessageLoop::current()->PostTask( | |
464 FROM_HERE, | |
465 base::Bind( | |
466 &BalloonViewImpl::DelayedClose, weak_factory_.GetWeakPtr(), false)); | |
467 } | |
468 } | |
469 | |
470 gboolean BalloonViewImpl::OnDestroy(GtkWidget* widget) { | |
471 frame_container_ = NULL; | |
472 Close(false); | |
473 return FALSE; // Propagate. | |
474 } | |
OLD | NEW |