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