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