Home | History | Annotate | Download | only in notifications
      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 
     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 
    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     MessageLoop::current()->PostTask(
    124         FROM_HERE,
    125         method_factory_.NewRunnableMethod(
    126             &BalloonViewImpl::DelayedClose, by_user));
    127   }
    128 }
    129 
    130 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 
    142 BalloonHost* BalloonViewImpl::GetHost() const {
    143   return html_contents_.get();
    144 }
    145 
    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 
    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 
    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 
    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 
    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 
    350 gfx::Point BalloonViewImpl::GetContentsOffset() const {
    351   return gfx::Point(kLeftShadowWidth + kLeftMargin,
    352                     GetShelfHeight() + kTopShadowWidth + kTopMargin);
    353 }
    354 
    355 int BalloonViewImpl::GetShelfHeight() const {
    356   // TODO(johnnyg): add scaling here.
    357   return kDefaultShelfHeight;
    358 }
    359 
    360 int BalloonViewImpl::GetDesiredTotalWidth() const {
    361   return balloon_->content_size().width() +
    362       kLeftMargin + kRightMargin + kLeftShadowWidth + kRightShadowWidth;
    363 }
    364 
    365 int BalloonViewImpl::GetDesiredTotalHeight() const {
    366   return balloon_->content_size().height() +
    367       kTopMargin + kBottomMargin + kTopShadowWidth + kBottomShadowWidth +
    368       GetShelfHeight();
    369 }
    370 
    371 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 
    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 
    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.
    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 
    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 
    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.
    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 
    476 gboolean BalloonViewImpl::OnDestroy(GtkWidget* widget) {
    477   frame_container_ = NULL;
    478   Close(false);
    479   return FALSE;  // Propagate.
    480 }
    481