// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/ui/gtk/notifications/balloon_view_gtk.h" #include <gtk/gtk.h> #include <string> #include <vector> #include "base/message_loop.h" #include "base/string_util.h" #include "chrome/browser/extensions/extension_host.h" #include "chrome/browser/extensions/extension_process_manager.h" #include "chrome/browser/notifications/balloon.h" #include "chrome/browser/notifications/desktop_notification_service.h" #include "chrome/browser/notifications/notification.h" #include "chrome/browser/notifications/notification_options_menu_model.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/themes/theme_service.h" #include "chrome/browser/ui/browser_list.h" #include "chrome/browser/ui/browser_window.h" #include "chrome/browser/ui/gtk/custom_button.h" #include "chrome/browser/ui/gtk/gtk_theme_service.h" #include "chrome/browser/ui/gtk/gtk_util.h" #include "chrome/browser/ui/gtk/info_bubble_gtk.h" #include "chrome/browser/ui/gtk/menu_gtk.h" #include "chrome/browser/ui/gtk/notifications/balloon_view_host_gtk.h" #include "chrome/browser/ui/gtk/rounded_window.h" #include "chrome/common/extensions/extension.h" #include "content/browser/renderer_host/render_view_host.h" #include "content/browser/renderer_host/render_widget_host_view.h" #include "content/common/notification_details.h" #include "content/common/notification_service.h" #include "content/common/notification_source.h" #include "content/common/notification_type.h" #include "grit/generated_resources.h" #include "grit/theme_resources.h" #include "ui/base/animation/slide_animation.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/resource/resource_bundle.h" #include "ui/gfx/canvas.h" #include "ui/gfx/insets.h" #include "ui/gfx/native_widget_types.h" namespace { // Margin, in pixels, between the notification frame and the contents // of the notification. const int kTopMargin = 0; const int kBottomMargin = 1; const int kLeftMargin = 1; const int kRightMargin = 1; // How many pixels of overlap there is between the shelf top and the // balloon bottom. const int kShelfBorderTopOverlap = 0; // Properties of the origin label. const int kLeftLabelMargin = 8; // TODO(johnnyg): Add a shadow for the frame. const int kLeftShadowWidth = 0; const int kRightShadowWidth = 0; const int kTopShadowWidth = 0; const int kBottomShadowWidth = 0; // Space in pixels between text and icon on the buttons. const int kButtonSpacing = 4; // Number of characters to show in the origin label before ellipsis. const int kOriginLabelCharacters = 18; // The shelf height for the system default font size. It is scaled // with changes in the default font size. const int kDefaultShelfHeight = 21; const int kShelfVerticalMargin = 4; // The amount that the bubble collections class offsets from the side of the // screen. const int kScreenBorder = 5; // Colors specified in various ways for different parts of the UI. // These match the windows colors in balloon_view.cc const char* kLabelColor = "#7D7D7D"; const double kShelfBackgroundColorR = 245.0 / 255.0; const double kShelfBackgroundColorG = 245.0 / 255.0; const double kShelfBackgroundColorB = 245.0 / 255.0; const double kDividerLineColorR = 180.0 / 255.0; const double kDividerLineColorG = 180.0 / 255.0; const double kDividerLineColorB = 180.0 / 255.0; // Makes the website label relatively smaller to the base text size. const char* kLabelMarkup = "<span size=\"small\" color=\"%s\">%s</span>"; } // namespace BalloonViewImpl::BalloonViewImpl(BalloonCollection* collection) : balloon_(NULL), frame_container_(NULL), html_container_(NULL), method_factory_(this), close_button_(NULL), animation_(NULL), menu_showing_(false), pending_close_(false) { } BalloonViewImpl::~BalloonViewImpl() { if (frame_container_) { GtkWidget* widget = frame_container_; frame_container_ = NULL; gtk_widget_hide(widget); } } void BalloonViewImpl::Close(bool by_user) { // Delay a system-initiated close if the menu is showing. if (!by_user && menu_showing_) { pending_close_ = true; } else { MessageLoop::current()->PostTask( FROM_HERE, method_factory_.NewRunnableMethod( &BalloonViewImpl::DelayedClose, by_user)); } } gfx::Size BalloonViewImpl::GetSize() const { // BalloonView has no size if it hasn't been shown yet (which is when // balloon_ is set). if (!balloon_) return gfx::Size(); // Although this may not be the instantaneous size of the balloon if // called in the middle of an animation, it is the effective size that // will result from the animation. return gfx::Size(GetDesiredTotalWidth(), GetDesiredTotalHeight()); } BalloonHost* BalloonViewImpl::GetHost() const { return html_contents_.get(); } void BalloonViewImpl::DelayedClose(bool by_user) { html_contents_->Shutdown(); if (frame_container_) { // It's possible that |frame_container_| was destroyed before the // BalloonViewImpl if our related browser window was closed first. gtk_widget_hide(frame_container_); } balloon_->OnClose(by_user); } void BalloonViewImpl::RepositionToBalloon() { if (!frame_container_) { // No need to create a slide animation when this balloon is fading out. return; } DCHECK(balloon_); // Create an amination from the current position to the desired one. int start_x; int start_y; int start_w; int start_h; gtk_window_get_position(GTK_WINDOW(frame_container_), &start_x, &start_y); gtk_window_get_size(GTK_WINDOW(frame_container_), &start_w, &start_h); int end_x = balloon_->GetPosition().x(); int end_y = balloon_->GetPosition().y(); int end_w = GetDesiredTotalWidth(); int end_h = GetDesiredTotalHeight(); anim_frame_start_ = gfx::Rect(start_x, start_y, start_w, start_h); anim_frame_end_ = gfx::Rect(end_x, end_y, end_w, end_h); animation_.reset(new ui::SlideAnimation(this)); animation_->Show(); } void BalloonViewImpl::AnimationProgressed(const ui::Animation* animation) { DCHECK_EQ(animation, animation_.get()); // Linear interpolation from start to end position. double end = animation->GetCurrentValue(); double start = 1.0 - end; gfx::Rect frame_position( static_cast<int>(start * anim_frame_start_.x() + end * anim_frame_end_.x()), static_cast<int>(start * anim_frame_start_.y() + end * anim_frame_end_.y()), static_cast<int>(start * anim_frame_start_.width() + end * anim_frame_end_.width()), static_cast<int>(start * anim_frame_start_.height() + end * anim_frame_end_.height())); gtk_window_resize(GTK_WINDOW(frame_container_), frame_position.width(), frame_position.height()); gtk_window_move(GTK_WINDOW(frame_container_), frame_position.x(), frame_position.y()); gfx::Rect contents_rect = GetContentsRectangle(); html_contents_->UpdateActualSize(contents_rect.size()); } void BalloonViewImpl::Show(Balloon* balloon) { theme_service_ = GtkThemeService::GetFrom(balloon->profile()); const std::string source_label_text = l10n_util::GetStringFUTF8( IDS_NOTIFICATION_BALLOON_SOURCE_LABEL, balloon->notification().display_source()); const std::string options_text = l10n_util::GetStringUTF8(IDS_NOTIFICATION_OPTIONS_MENU_LABEL); const std::string dismiss_text = l10n_util::GetStringUTF8(IDS_NOTIFICATION_BALLOON_DISMISS_LABEL); balloon_ = balloon; frame_container_ = gtk_window_new(GTK_WINDOW_POPUP); // Construct the options menu. options_menu_model_.reset(new NotificationOptionsMenuModel(balloon_)); options_menu_.reset(new MenuGtk(this, options_menu_model_.get())); // Create a BalloonViewHost to host the HTML contents of this balloon. html_contents_.reset(new BalloonViewHost(balloon)); html_contents_->Init(); gfx::NativeView contents = html_contents_->native_view(); g_signal_connect_after(contents, "expose-event", G_CALLBACK(OnContentsExposeThunk), this); // Divide the frame vertically into the shelf and the content area. GtkWidget* vbox = gtk_vbox_new(0, 0); gtk_container_add(GTK_CONTAINER(frame_container_), vbox); shelf_ = gtk_hbox_new(0, 0); gtk_container_add(GTK_CONTAINER(vbox), shelf_); GtkWidget* alignment = gtk_alignment_new(0.0, 0.0, 1.0, 1.0); gtk_alignment_set_padding( GTK_ALIGNMENT(alignment), kTopMargin, kBottomMargin, kLeftMargin, kRightMargin); gtk_widget_show_all(alignment); gtk_container_add(GTK_CONTAINER(alignment), contents); gtk_container_add(GTK_CONTAINER(vbox), alignment); // Create a toolbar and add it to the shelf. hbox_ = gtk_hbox_new(FALSE, 0); gtk_widget_set_size_request(GTK_WIDGET(hbox_), -1, GetShelfHeight()); gtk_container_add(GTK_CONTAINER(shelf_), hbox_); gtk_widget_show_all(vbox); g_signal_connect(frame_container_, "expose-event", G_CALLBACK(OnExposeThunk), this); g_signal_connect(frame_container_, "destroy", G_CALLBACK(OnDestroyThunk), this); // Create a label for the source of the notification and add it to the // toolbar. GtkWidget* source_label_ = gtk_label_new(NULL); char* markup = g_markup_printf_escaped(kLabelMarkup, kLabelColor, source_label_text.c_str()); gtk_label_set_markup(GTK_LABEL(source_label_), markup); g_free(markup); gtk_label_set_max_width_chars(GTK_LABEL(source_label_), kOriginLabelCharacters); gtk_label_set_ellipsize(GTK_LABEL(source_label_), PANGO_ELLIPSIZE_END); GtkWidget* label_alignment = gtk_alignment_new(0.0, 0.0, 1.0, 1.0); gtk_alignment_set_padding(GTK_ALIGNMENT(label_alignment), kShelfVerticalMargin, kShelfVerticalMargin, kLeftLabelMargin, 0); gtk_container_add(GTK_CONTAINER(label_alignment), source_label_); gtk_box_pack_start(GTK_BOX(hbox_), label_alignment, FALSE, FALSE, 0); ResourceBundle& rb = ResourceBundle::GetSharedInstance(); // Create a button to dismiss the balloon and add it to the toolbar. close_button_.reset(new CustomDrawButton(IDR_TAB_CLOSE, IDR_TAB_CLOSE_P, IDR_TAB_CLOSE_H, IDR_TAB_CLOSE)); close_button_->SetBackground(SK_ColorBLACK, rb.GetBitmapNamed(IDR_TAB_CLOSE), rb.GetBitmapNamed(IDR_TAB_CLOSE_MASK)); gtk_widget_set_tooltip_text(close_button_->widget(), dismiss_text.c_str()); g_signal_connect(close_button_->widget(), "clicked", G_CALLBACK(OnCloseButtonThunk), this); GTK_WIDGET_UNSET_FLAGS(close_button_->widget(), GTK_CAN_FOCUS); GtkWidget* close_alignment = gtk_alignment_new(0.0, 0.0, 1.0, 1.0); gtk_alignment_set_padding(GTK_ALIGNMENT(close_alignment), kShelfVerticalMargin, kShelfVerticalMargin, 0, kButtonSpacing); gtk_container_add(GTK_CONTAINER(close_alignment), close_button_->widget()); gtk_box_pack_end(GTK_BOX(hbox_), close_alignment, FALSE, FALSE, 0); // Create a button for showing the options menu, and add it to the toolbar. options_menu_button_.reset(new CustomDrawButton(IDR_BALLOON_WRENCH, IDR_BALLOON_WRENCH_P, IDR_BALLOON_WRENCH_H, 0)); gtk_widget_set_tooltip_text(options_menu_button_->widget(), options_text.c_str()); g_signal_connect(options_menu_button_->widget(), "button-press-event", G_CALLBACK(OnOptionsMenuButtonThunk), this); GTK_WIDGET_UNSET_FLAGS(options_menu_button_->widget(), GTK_CAN_FOCUS); GtkWidget* options_alignment = gtk_alignment_new(0.0, 0.0, 1.0, 1.0); gtk_alignment_set_padding(GTK_ALIGNMENT(options_alignment), kShelfVerticalMargin, kShelfVerticalMargin, 0, kButtonSpacing); gtk_container_add(GTK_CONTAINER(options_alignment), options_menu_button_->widget()); gtk_box_pack_end(GTK_BOX(hbox_), options_alignment, FALSE, FALSE, 0); notification_registrar_.Add(this, NotificationType::BROWSER_THEME_CHANGED, NotificationService::AllSources()); // We don't do InitThemesFor() because it just forces a redraw. gtk_util::ActAsRoundedWindow(frame_container_, gtk_util::kGdkBlack, 3, gtk_util::ROUNDED_ALL, gtk_util::BORDER_ALL); // Realize the frame container so we can do size calculations. gtk_widget_realize(frame_container_); // Update to make sure we have everything sized properly and then move our // window offscreen for its initial animation. html_contents_->UpdateActualSize(balloon_->content_size()); int window_width; gtk_window_get_size(GTK_WINDOW(frame_container_), &window_width, NULL); int pos_x = gdk_screen_width() - window_width - kScreenBorder; int pos_y = gdk_screen_height(); gtk_window_move(GTK_WINDOW(frame_container_), pos_x, pos_y); balloon_->SetPosition(gfx::Point(pos_x, pos_y), false); gtk_widget_show_all(frame_container_); notification_registrar_.Add(this, NotificationType::NOTIFY_BALLOON_DISCONNECTED, Source<Balloon>(balloon)); } void BalloonViewImpl::Update() { DCHECK(html_contents_.get()) << "BalloonView::Update called before Show"; if (html_contents_->render_view_host()) html_contents_->render_view_host()->NavigateToURL( balloon_->notification().content_url()); } gfx::Point BalloonViewImpl::GetContentsOffset() const { return gfx::Point(kLeftShadowWidth + kLeftMargin, GetShelfHeight() + kTopShadowWidth + kTopMargin); } int BalloonViewImpl::GetShelfHeight() const { // TODO(johnnyg): add scaling here. return kDefaultShelfHeight; } int BalloonViewImpl::GetDesiredTotalWidth() const { return balloon_->content_size().width() + kLeftMargin + kRightMargin + kLeftShadowWidth + kRightShadowWidth; } int BalloonViewImpl::GetDesiredTotalHeight() const { return balloon_->content_size().height() + kTopMargin + kBottomMargin + kTopShadowWidth + kBottomShadowWidth + GetShelfHeight(); } gfx::Rect BalloonViewImpl::GetContentsRectangle() const { if (!frame_container_) return gfx::Rect(); gfx::Size content_size = balloon_->content_size(); gfx::Point offset = GetContentsOffset(); int x = 0, y = 0; gtk_window_get_position(GTK_WINDOW(frame_container_), &x, &y); return gfx::Rect(x + offset.x(), y + offset.y(), content_size.width(), content_size.height()); } void BalloonViewImpl::Observe(NotificationType type, const NotificationSource& source, const NotificationDetails& details) { if (type == NotificationType::NOTIFY_BALLOON_DISCONNECTED) { // If the renderer process attached to this balloon is disconnected // (e.g., because of a crash), we want to close the balloon. notification_registrar_.Remove(this, NotificationType::NOTIFY_BALLOON_DISCONNECTED, Source<Balloon>(balloon_)); Close(false); } else if (type == NotificationType::BROWSER_THEME_CHANGED) { // Since all the buttons change their own properties, and our expose does // all the real differences, we'll need a redraw. gtk_widget_queue_draw(frame_container_); } else { NOTREACHED(); } } void BalloonViewImpl::OnCloseButton(GtkWidget* widget) { Close(true); } // We draw black dots on the bottom left and right corners to fill in the // border. Otherwise, the border has a gap because the sharp corners of the // HTML view cut off the roundedness of the notification window. gboolean BalloonViewImpl::OnContentsExpose(GtkWidget* sender, GdkEventExpose* event) { cairo_t* cr = gdk_cairo_create(GDK_DRAWABLE(sender->window)); gdk_cairo_rectangle(cr, &event->area); cairo_clip(cr); // According to a discussion on a mailing list I found, these degenerate // paths are the officially supported way to draw points in Cairo. cairo_set_source_rgb(cr, 0, 0, 0); cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); cairo_set_line_width(cr, 1.0); cairo_move_to(cr, 0.5, sender->allocation.height - 0.5); cairo_close_path(cr); cairo_move_to(cr, sender->allocation.width - 0.5, sender->allocation.height - 0.5); cairo_close_path(cr); cairo_stroke(cr); cairo_destroy(cr); return FALSE; } gboolean BalloonViewImpl::OnExpose(GtkWidget* sender, GdkEventExpose* event) { cairo_t* cr = gdk_cairo_create(GDK_DRAWABLE(sender->window)); gdk_cairo_rectangle(cr, &event->area); cairo_clip(cr); gfx::Size content_size = balloon_->content_size(); gfx::Point offset = GetContentsOffset(); // Draw a background color behind the shelf. cairo_set_source_rgb(cr, kShelfBackgroundColorR, kShelfBackgroundColorG, kShelfBackgroundColorB); cairo_rectangle(cr, kLeftMargin, kTopMargin + 0.5, content_size.width() - 0.5, GetShelfHeight()); cairo_fill(cr); // Now draw a one pixel line between content and shelf. cairo_move_to(cr, offset.x(), offset.y() - 1); cairo_line_to(cr, offset.x() + content_size.width(), offset.y() - 1); cairo_set_line_width(cr, 0.5); cairo_set_source_rgb(cr, kDividerLineColorR, kDividerLineColorG, kDividerLineColorB); cairo_stroke(cr); cairo_destroy(cr); return FALSE; } void BalloonViewImpl::OnOptionsMenuButton(GtkWidget* widget, GdkEventButton* event) { menu_showing_ = true; options_menu_->PopupForWidget(widget, event->button, event->time); } // Called when the menu stops showing. void BalloonViewImpl::StoppedShowing() { menu_showing_ = false; if (pending_close_) { MessageLoop::current()->PostTask( FROM_HERE, method_factory_.NewRunnableMethod( &BalloonViewImpl::DelayedClose, false)); } } gboolean BalloonViewImpl::OnDestroy(GtkWidget* widget) { frame_container_ = NULL; Close(false); return FALSE; // Propagate. }