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