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