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
47namespace {
48
49// Margin, in pixels, between the notification frame and the contents
50// of the notification.
51const int kTopMargin = 0;
52const int kBottomMargin = 1;
53const int kLeftMargin = 1;
54const int kRightMargin = 1;
55
56// How many pixels of overlap there is between the shelf top and the
57// balloon bottom.
58const int kShelfBorderTopOverlap = 0;
59
60// Properties of the origin label.
61const int kLeftLabelMargin = 8;
62
63// TODO(johnnyg): Add a shadow for the frame.
64const int kLeftShadowWidth = 0;
65const int kRightShadowWidth = 0;
66const int kTopShadowWidth = 0;
67const int kBottomShadowWidth = 0;
68
69// Space in pixels between text and icon on the buttons.
70const int kButtonSpacing = 4;
71
72// Number of characters to show in the origin label before ellipsis.
73const 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.
77const int kDefaultShelfHeight = 21;
78const int kShelfVerticalMargin = 4;
79
80// The amount that the bubble collections class offsets from the side of the
81// screen.
82const 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
86const char* kLabelColor = "#7D7D7D";
87const double kShelfBackgroundColorR = 245.0 / 255.0;
88const double kShelfBackgroundColorG = 245.0 / 255.0;
89const double kShelfBackgroundColorB = 245.0 / 255.0;
90const double kDividerLineColorR = 180.0 / 255.0;
91const double kDividerLineColorG = 180.0 / 255.0;
92const double kDividerLineColorB = 180.0 / 255.0;
93
94// Makes the website label relatively smaller to the base text size.
95const char* kLabelMarkup = "<span size=\"small\" color=\"%s\">%s</span>";
96
97}  // namespace
98
99BalloonViewImpl::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
110BalloonViewImpl::~BalloonViewImpl() {
111  if (frame_container_) {
112    GtkWidget* widget = frame_container_;
113    frame_container_ = NULL;
114    gtk_widget_hide(widget);
115  }
116}
117
118void 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
130gfx::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
142BalloonHost* BalloonViewImpl::GetHost() const {
143  return html_contents_.get();
144}
145
146void 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
156void 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
183void 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
208void 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
343void 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
350gfx::Point BalloonViewImpl::GetContentsOffset() const {
351  return gfx::Point(kLeftShadowWidth + kLeftMargin,
352                    GetShelfHeight() + kTopShadowWidth + kTopMargin);
353}
354
355int BalloonViewImpl::GetShelfHeight() const {
356  // TODO(johnnyg): add scaling here.
357  return kDefaultShelfHeight;
358}
359
360int BalloonViewImpl::GetDesiredTotalWidth() const {
361  return balloon_->content_size().width() +
362      kLeftMargin + kRightMargin + kLeftShadowWidth + kRightShadowWidth;
363}
364
365int BalloonViewImpl::GetDesiredTotalHeight() const {
366  return balloon_->content_size().height() +
367      kTopMargin + kBottomMargin + kTopShadowWidth + kBottomShadowWidth +
368      GetShelfHeight();
369}
370
371gfx::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
383void 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
402void 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.
409gboolean 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
431gboolean 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
459void 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.
466void 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
476gboolean BalloonViewImpl::OnDestroy(GtkWidget* widget) {
477  frame_container_ = NULL;
478  Close(false);
479  return FALSE;  // Propagate.
480}
481