| // Copyright (c) 2012 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/bind.h" |
| #include "base/debug/trace_event.h" |
| #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/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/chrome_notification_types.h" |
| #include "chrome/common/extensions/extension.h" |
| #include "content/public/browser/notification_source.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/browser/render_widget_host_view.h" |
| #include "content/public/browser/web_contents.h" |
| #include "grit/generated_resources.h" |
| #include "grit/theme_resources.h" |
| #include "ui/base/animation/slide_animation.h" |
| #include "ui/base/gtk/gtk_hig_constants.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), |
| weak_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, |
| base::Bind(&BalloonViewImpl::DelayedClose, |
| weak_factory_.GetWeakPtr(), |
| 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); |
| |
| ui::ResourceBundle& rb = ui::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_set_can_focus(close_button_->widget(), FALSE); |
| 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_set_can_focus(options_menu_button_->widget(), FALSE); |
| 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, chrome::NOTIFICATION_BROWSER_THEME_CHANGED, |
| content::Source<ThemeService>(theme_service_)); |
| |
| // We don't do InitThemesFor() because it just forces a redraw. |
| gtk_util::ActAsRoundedWindow(frame_container_, ui::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, |
| chrome::NOTIFICATION_NOTIFY_BALLOON_DISCONNECTED, |
| content::Source<Balloon>(balloon)); |
| } |
| |
| void BalloonViewImpl::Update() { |
| DCHECK(html_contents_.get()) << "BalloonView::Update called before Show"; |
| if (!html_contents_->web_contents()) |
| return; |
| html_contents_->web_contents()->GetController().LoadURL( |
| balloon_->notification().content_url(), content::Referrer(), |
| content::PAGE_TRANSITION_LINK, std::string()); |
| } |
| |
| 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(int type, |
| const content::NotificationSource& source, |
| const content::NotificationDetails& details) { |
| if (type == chrome::NOTIFICATION_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, |
| chrome::NOTIFICATION_NOTIFY_BALLOON_DISCONNECTED, |
| content::Source<Balloon>(balloon_)); |
| Close(false); |
| } else if (type == chrome::NOTIFICATION_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) { |
| TRACE_EVENT0("ui::gtk", "BalloonViewImpl::OnContentsExpose"); |
| cairo_t* cr = gdk_cairo_create(gtk_widget_get_window(sender)); |
| gdk_cairo_rectangle(cr, &event->area); |
| cairo_clip(cr); |
| |
| GtkAllocation allocation; |
| gtk_widget_get_allocation(sender, &allocation); |
| |
| // 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, allocation.height - 0.5); |
| cairo_close_path(cr); |
| cairo_move_to(cr, allocation.width - 0.5, allocation.height - 0.5); |
| cairo_close_path(cr); |
| cairo_stroke(cr); |
| cairo_destroy(cr); |
| |
| return FALSE; |
| } |
| |
| gboolean BalloonViewImpl::OnExpose(GtkWidget* sender, GdkEventExpose* event) { |
| TRACE_EVENT0("ui::gtk", "BalloonViewImpl::OnExpose"); |
| cairo_t* cr = gdk_cairo_create(gtk_widget_get_window(sender)); |
| 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, |
| base::Bind(&BalloonViewImpl::DelayedClose, |
| weak_factory_.GetWeakPtr(), |
| false)); |
| } |
| } |
| |
| gboolean BalloonViewImpl::OnDestroy(GtkWidget* widget) { |
| frame_container_ = NULL; |
| Close(false); |
| return FALSE; // Propagate. |
| } |