| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2010 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/gtk/notifications/balloon_view_gtk.h" | |
| 6 | |
| 7 #include <gtk/gtk.h> | |
| 8 | |
| 9 #include <string> | |
| 10 #include <vector> | |
| 11 | |
| 12 #include "app/l10n_util.h" | |
| 13 #include "app/resource_bundle.h" | |
| 14 #include "base/message_loop.h" | |
| 15 #include "base/string_util.h" | |
| 16 #include "chrome/browser/browser_list.h" | |
| 17 #include "chrome/browser/browser_window.h" | |
| 18 #include "chrome/browser/extensions/extension_host.h" | |
| 19 #include "chrome/browser/extensions/extension_process_manager.h" | |
| 20 #include "chrome/browser/gtk/custom_button.h" | |
| 21 #include "chrome/browser/gtk/gtk_theme_provider.h" | |
| 22 #include "chrome/browser/gtk/gtk_util.h" | |
| 23 #include "chrome/browser/gtk/info_bubble_gtk.h" | |
| 24 #include "chrome/browser/gtk/menu_gtk.h" | |
| 25 #include "chrome/browser/gtk/notifications/balloon_view_host_gtk.h" | |
| 26 #include "chrome/browser/gtk/rounded_window.h" | |
| 27 #include "chrome/browser/notifications/balloon.h" | |
| 28 #include "chrome/browser/notifications/desktop_notification_service.h" | |
| 29 #include "chrome/browser/notifications/notification.h" | |
| 30 #include "chrome/browser/notifications/notification_options_menu_model.h" | |
| 31 #include "chrome/browser/profiles/profile.h" | |
| 32 #include "chrome/browser/renderer_host/render_view_host.h" | |
| 33 #include "chrome/browser/renderer_host/render_widget_host_view.h" | |
| 34 #include "chrome/browser/themes/browser_theme_provider.h" | |
| 35 #include "chrome/common/extensions/extension.h" | |
| 36 #include "chrome/common/notification_details.h" | |
| 37 #include "chrome/common/notification_service.h" | |
| 38 #include "chrome/common/notification_source.h" | |
| 39 #include "chrome/common/notification_type.h" | |
| 40 #include "gfx/canvas.h" | |
| 41 #include "gfx/insets.h" | |
| 42 #include "gfx/native_widget_types.h" | |
| 43 #include "grit/generated_resources.h" | |
| 44 #include "grit/theme_resources.h" | |
| 45 #include "ui/base/animation/slide_animation.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 } | |
| 107 | |
| 108 BalloonViewImpl::~BalloonViewImpl() { | |
| 109 } | |
| 110 | |
| 111 void BalloonViewImpl::Close(bool by_user) { | |
| 112 MessageLoop::current()->PostTask(FROM_HERE, | |
| 113 method_factory_.NewRunnableMethod( | |
| 114 &BalloonViewImpl::DelayedClose, by_user)); | |
| 115 } | |
| 116 | |
| 117 gfx::Size BalloonViewImpl::GetSize() const { | |
| 118 // BalloonView has no size if it hasn't been shown yet (which is when | |
| 119 // balloon_ is set). | |
| 120 if (!balloon_) | |
| 121 return gfx::Size(); | |
| 122 | |
| 123 // Although this may not be the instantaneous size of the balloon if | |
| 124 // called in the middle of an animation, it is the effective size that | |
| 125 // will result from the animation. | |
| 126 return gfx::Size(GetDesiredTotalWidth(), GetDesiredTotalHeight()); | |
| 127 } | |
| 128 | |
| 129 BalloonHost* BalloonViewImpl::GetHost() const { | |
| 130 return html_contents_.get(); | |
| 131 } | |
| 132 | |
| 133 void BalloonViewImpl::DelayedClose(bool by_user) { | |
| 134 html_contents_->Shutdown(); | |
| 135 if (frame_container_) { | |
| 136 // It's possible that |frame_container_| was destroyed before the | |
| 137 // BalloonViewImpl if our related browser window was closed first. | |
| 138 gtk_widget_hide(frame_container_); | |
| 139 } | |
| 140 balloon_->OnClose(by_user); | |
| 141 } | |
| 142 | |
| 143 void BalloonViewImpl::RepositionToBalloon() { | |
| 144 if (!frame_container_) { | |
| 145 // No need to create a slide animation when this balloon is fading out. | |
| 146 return; | |
| 147 } | |
| 148 | |
| 149 DCHECK(balloon_); | |
| 150 | |
| 151 // Create an amination from the current position to the desired one. | |
| 152 int start_x; | |
| 153 int start_y; | |
| 154 int start_w; | |
| 155 int start_h; | |
| 156 gtk_window_get_position(GTK_WINDOW(frame_container_), &start_x, &start_y); | |
| 157 gtk_window_get_size(GTK_WINDOW(frame_container_), &start_w, &start_h); | |
| 158 | |
| 159 int end_x = balloon_->GetPosition().x(); | |
| 160 int end_y = balloon_->GetPosition().y(); | |
| 161 int end_w = GetDesiredTotalWidth(); | |
| 162 int end_h = GetDesiredTotalHeight(); | |
| 163 | |
| 164 anim_frame_start_ = gfx::Rect(start_x, start_y, start_w, start_h); | |
| 165 anim_frame_end_ = gfx::Rect(end_x, end_y, end_w, end_h); | |
| 166 animation_.reset(new ui::SlideAnimation(this)); | |
| 167 animation_->Show(); | |
| 168 } | |
| 169 | |
| 170 void BalloonViewImpl::AnimationProgressed(const ui::Animation* animation) { | |
| 171 DCHECK_EQ(animation, animation_.get()); | |
| 172 | |
| 173 // Linear interpolation from start to end position. | |
| 174 double end = animation->GetCurrentValue(); | |
| 175 double start = 1.0 - end; | |
| 176 | |
| 177 gfx::Rect frame_position( | |
| 178 static_cast<int>(start * anim_frame_start_.x() + | |
| 179 end * anim_frame_end_.x()), | |
| 180 static_cast<int>(start * anim_frame_start_.y() + | |
| 181 end * anim_frame_end_.y()), | |
| 182 static_cast<int>(start * anim_frame_start_.width() + | |
| 183 end * anim_frame_end_.width()), | |
| 184 static_cast<int>(start * anim_frame_start_.height() + | |
| 185 end * anim_frame_end_.height())); | |
| 186 gtk_window_resize(GTK_WINDOW(frame_container_), | |
| 187 frame_position.width(), frame_position.height()); | |
| 188 gtk_window_move(GTK_WINDOW(frame_container_), | |
| 189 frame_position.x(), frame_position.y()); | |
| 190 | |
| 191 gfx::Rect contents_rect = GetContentsRectangle(); | |
| 192 html_contents_->UpdateActualSize(contents_rect.size()); | |
| 193 } | |
| 194 | |
| 195 void BalloonViewImpl::Show(Balloon* balloon) { | |
| 196 theme_provider_ = GtkThemeProvider::GetFrom(balloon->profile()); | |
| 197 | |
| 198 const std::string source_label_text = l10n_util::GetStringFUTF8( | |
| 199 IDS_NOTIFICATION_BALLOON_SOURCE_LABEL, | |
| 200 balloon->notification().display_source()); | |
| 201 const std::string options_text = | |
| 202 l10n_util::GetStringUTF8(IDS_NOTIFICATION_OPTIONS_MENU_LABEL); | |
| 203 const std::string dismiss_text = | |
| 204 l10n_util::GetStringUTF8(IDS_NOTIFICATION_BALLOON_DISMISS_LABEL); | |
| 205 | |
| 206 balloon_ = balloon; | |
| 207 frame_container_ = gtk_window_new(GTK_WINDOW_POPUP); | |
| 208 | |
| 209 // Construct the options menu. | |
| 210 options_menu_model_.reset(new NotificationOptionsMenuModel(balloon_)); | |
| 211 options_menu_.reset(new MenuGtk(this, options_menu_model_.get())); | |
| 212 | |
| 213 // Create a BalloonViewHost to host the HTML contents of this balloon. | |
| 214 html_contents_.reset(new BalloonViewHost(balloon)); | |
| 215 html_contents_->Init(); | |
| 216 gfx::NativeView contents = html_contents_->native_view(); | |
| 217 | |
| 218 // Divide the frame vertically into the shelf and the content area. | |
| 219 GtkWidget* vbox = gtk_vbox_new(0, 0); | |
| 220 gtk_container_add(GTK_CONTAINER(frame_container_), vbox); | |
| 221 | |
| 222 shelf_ = gtk_hbox_new(0, 0); | |
| 223 gtk_container_add(GTK_CONTAINER(vbox), shelf_); | |
| 224 | |
| 225 GtkWidget* alignment = gtk_alignment_new(0.0, 0.0, 1.0, 1.0); | |
| 226 gtk_alignment_set_padding( | |
| 227 GTK_ALIGNMENT(alignment), | |
| 228 kTopMargin, kBottomMargin, kLeftMargin, kRightMargin); | |
| 229 gtk_widget_show_all(alignment); | |
| 230 gtk_container_add(GTK_CONTAINER(alignment), contents); | |
| 231 gtk_container_add(GTK_CONTAINER(vbox), alignment); | |
| 232 | |
| 233 // Create a toolbar and add it to the shelf. | |
| 234 hbox_ = gtk_hbox_new(FALSE, 0); | |
| 235 gtk_widget_set_size_request(GTK_WIDGET(hbox_), -1, GetShelfHeight()); | |
| 236 gtk_container_add(GTK_CONTAINER(shelf_), hbox_); | |
| 237 gtk_widget_show_all(vbox); | |
| 238 | |
| 239 g_signal_connect(frame_container_, "expose-event", | |
| 240 G_CALLBACK(OnExposeThunk), this); | |
| 241 g_signal_connect(frame_container_, "destroy", | |
| 242 G_CALLBACK(OnDestroyThunk), this); | |
| 243 | |
| 244 // Create a label for the source of the notification and add it to the | |
| 245 // toolbar. | |
| 246 GtkWidget* source_label_ = gtk_label_new(NULL); | |
| 247 char* markup = g_markup_printf_escaped(kLabelMarkup, | |
| 248 kLabelColor, | |
| 249 source_label_text.c_str()); | |
| 250 gtk_label_set_markup(GTK_LABEL(source_label_), markup); | |
| 251 g_free(markup); | |
| 252 gtk_label_set_max_width_chars(GTK_LABEL(source_label_), | |
| 253 kOriginLabelCharacters); | |
| 254 gtk_label_set_ellipsize(GTK_LABEL(source_label_), PANGO_ELLIPSIZE_END); | |
| 255 GtkWidget* label_alignment = gtk_alignment_new(0.0, 0.0, 1.0, 1.0); | |
| 256 gtk_alignment_set_padding(GTK_ALIGNMENT(label_alignment), | |
| 257 kShelfVerticalMargin, kShelfVerticalMargin, | |
| 258 kLeftLabelMargin, 0); | |
| 259 gtk_container_add(GTK_CONTAINER(label_alignment), source_label_); | |
| 260 gtk_box_pack_start(GTK_BOX(hbox_), label_alignment, FALSE, FALSE, 0); | |
| 261 | |
| 262 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); | |
| 263 | |
| 264 // Create a button to dismiss the balloon and add it to the toolbar. | |
| 265 close_button_.reset(new CustomDrawButton(IDR_TAB_CLOSE, | |
| 266 IDR_TAB_CLOSE_P, | |
| 267 IDR_TAB_CLOSE_H, | |
| 268 IDR_TAB_CLOSE)); | |
| 269 close_button_->SetBackground(SK_ColorBLACK, | |
| 270 rb.GetBitmapNamed(IDR_TAB_CLOSE), | |
| 271 rb.GetBitmapNamed(IDR_TAB_CLOSE_MASK)); | |
| 272 gtk_widget_set_tooltip_text(close_button_->widget(), dismiss_text.c_str()); | |
| 273 g_signal_connect(close_button_->widget(), "clicked", | |
| 274 G_CALLBACK(OnCloseButtonThunk), this); | |
| 275 GTK_WIDGET_UNSET_FLAGS(close_button_->widget(), GTK_CAN_FOCUS); | |
| 276 GtkWidget* close_alignment = gtk_alignment_new(0.0, 0.0, 1.0, 1.0); | |
| 277 gtk_alignment_set_padding(GTK_ALIGNMENT(close_alignment), | |
| 278 kShelfVerticalMargin, kShelfVerticalMargin, | |
| 279 0, kButtonSpacing); | |
| 280 gtk_container_add(GTK_CONTAINER(close_alignment), close_button_->widget()); | |
| 281 gtk_box_pack_end(GTK_BOX(hbox_), close_alignment, FALSE, FALSE, 0); | |
| 282 | |
| 283 // Create a button for showing the options menu, and add it to the toolbar. | |
| 284 options_menu_button_.reset(new CustomDrawButton(IDR_BALLOON_WRENCH, | |
| 285 IDR_BALLOON_WRENCH_P, | |
| 286 IDR_BALLOON_WRENCH_H, | |
| 287 0)); | |
| 288 gtk_widget_set_tooltip_text(options_menu_button_->widget(), | |
| 289 options_text.c_str()); | |
| 290 g_signal_connect(options_menu_button_->widget(), "clicked", | |
| 291 G_CALLBACK(OnOptionsMenuButtonThunk), this); | |
| 292 GTK_WIDGET_UNSET_FLAGS(options_menu_button_->widget(), GTK_CAN_FOCUS); | |
| 293 GtkWidget* options_alignment = gtk_alignment_new(0.0, 0.0, 1.0, 1.0); | |
| 294 gtk_alignment_set_padding(GTK_ALIGNMENT(options_alignment), | |
| 295 kShelfVerticalMargin, kShelfVerticalMargin, | |
| 296 0, kButtonSpacing); | |
| 297 gtk_container_add(GTK_CONTAINER(options_alignment), | |
| 298 options_menu_button_->widget()); | |
| 299 gtk_box_pack_end(GTK_BOX(hbox_), options_alignment, FALSE, FALSE, 0); | |
| 300 | |
| 301 notification_registrar_.Add(this, NotificationType::BROWSER_THEME_CHANGED, | |
| 302 NotificationService::AllSources()); | |
| 303 | |
| 304 // We don't do InitThemesFor() because it just forces a redraw. | |
| 305 gtk_util::ActAsRoundedWindow(frame_container_, gtk_util::kGdkBlack, 3, | |
| 306 gtk_util::ROUNDED_ALL, | |
| 307 gtk_util::BORDER_ALL); | |
| 308 | |
| 309 // Realize the frame container so we can do size calculations. | |
| 310 gtk_widget_realize(frame_container_); | |
| 311 | |
| 312 // Update to make sure we have everything sized properly and then move our | |
| 313 // window offscreen for its initial animation. | |
| 314 html_contents_->UpdateActualSize(balloon_->content_size()); | |
| 315 int window_width; | |
| 316 gtk_window_get_size(GTK_WINDOW(frame_container_), &window_width, NULL); | |
| 317 | |
| 318 int pos_x = gdk_screen_width() - window_width - kScreenBorder; | |
| 319 int pos_y = gdk_screen_height(); | |
| 320 gtk_window_move(GTK_WINDOW(frame_container_), pos_x, pos_y); | |
| 321 balloon_->SetPosition(gfx::Point(pos_x, pos_y), false); | |
| 322 gtk_widget_show_all(frame_container_); | |
| 323 | |
| 324 notification_registrar_.Add(this, | |
| 325 NotificationType::NOTIFY_BALLOON_DISCONNECTED, Source<Balloon>(balloon)); | |
| 326 } | |
| 327 | |
| 328 void BalloonViewImpl::Update() { | |
| 329 DCHECK(html_contents_.get()) << "BalloonView::Update called before Show"; | |
| 330 if (html_contents_->render_view_host()) | |
| 331 html_contents_->render_view_host()->NavigateToURL( | |
| 332 balloon_->notification().content_url()); | |
| 333 } | |
| 334 | |
| 335 gfx::Point BalloonViewImpl::GetContentsOffset() const { | |
| 336 return gfx::Point(kLeftShadowWidth + kLeftMargin, | |
| 337 GetShelfHeight() + kTopShadowWidth + kTopMargin); | |
| 338 } | |
| 339 | |
| 340 int BalloonViewImpl::GetShelfHeight() const { | |
| 341 // TODO(johnnyg): add scaling here. | |
| 342 return kDefaultShelfHeight; | |
| 343 } | |
| 344 | |
| 345 int BalloonViewImpl::GetDesiredTotalWidth() const { | |
| 346 return balloon_->content_size().width() + | |
| 347 kLeftMargin + kRightMargin + kLeftShadowWidth + kRightShadowWidth; | |
| 348 } | |
| 349 | |
| 350 int BalloonViewImpl::GetDesiredTotalHeight() const { | |
| 351 return balloon_->content_size().height() + | |
| 352 kTopMargin + kBottomMargin + kTopShadowWidth + kBottomShadowWidth + | |
| 353 GetShelfHeight(); | |
| 354 } | |
| 355 | |
| 356 gfx::Rect BalloonViewImpl::GetContentsRectangle() const { | |
| 357 if (!frame_container_) | |
| 358 return gfx::Rect(); | |
| 359 | |
| 360 gfx::Size content_size = balloon_->content_size(); | |
| 361 gfx::Point offset = GetContentsOffset(); | |
| 362 int x = 0, y = 0; | |
| 363 gtk_window_get_position(GTK_WINDOW(frame_container_), &x, &y); | |
| 364 return gfx::Rect(x + offset.x(), y + offset.y(), | |
| 365 content_size.width(), content_size.height()); | |
| 366 } | |
| 367 | |
| 368 void BalloonViewImpl::Observe(NotificationType type, | |
| 369 const NotificationSource& source, | |
| 370 const NotificationDetails& details) { | |
| 371 if (type == NotificationType::NOTIFY_BALLOON_DISCONNECTED) { | |
| 372 // If the renderer process attached to this balloon is disconnected | |
| 373 // (e.g., because of a crash), we want to close the balloon. | |
| 374 notification_registrar_.Remove(this, | |
| 375 NotificationType::NOTIFY_BALLOON_DISCONNECTED, | |
| 376 Source<Balloon>(balloon_)); | |
| 377 Close(false); | |
| 378 } else if (type == NotificationType::BROWSER_THEME_CHANGED) { | |
| 379 // Since all the buttons change their own properties, and our expose does | |
| 380 // all the real differences, we'll need a redraw. | |
| 381 gtk_widget_queue_draw(frame_container_); | |
| 382 } else { | |
| 383 NOTREACHED(); | |
| 384 } | |
| 385 } | |
| 386 | |
| 387 void BalloonViewImpl::OnCloseButton(GtkWidget* widget) { | |
| 388 Close(true); | |
| 389 } | |
| 390 | |
| 391 gboolean BalloonViewImpl::OnExpose(GtkWidget* sender, GdkEventExpose* event) { | |
| 392 cairo_t* cr = gdk_cairo_create(GDK_DRAWABLE(sender->window)); | |
| 393 gdk_cairo_rectangle(cr, &event->area); | |
| 394 cairo_clip(cr); | |
| 395 | |
| 396 gfx::Size content_size = balloon_->content_size(); | |
| 397 gfx::Point offset = GetContentsOffset(); | |
| 398 | |
| 399 // Draw a background color behind the shelf. | |
| 400 cairo_set_source_rgb(cr, kShelfBackgroundColorR, | |
| 401 kShelfBackgroundColorG, kShelfBackgroundColorB); | |
| 402 cairo_rectangle(cr, kLeftMargin, kTopMargin + 0.5, | |
| 403 content_size.width() - 0.5, GetShelfHeight()); | |
| 404 cairo_fill(cr); | |
| 405 | |
| 406 // Now draw a one pixel line between content and shelf. | |
| 407 cairo_move_to(cr, offset.x(), offset.y() - 1); | |
| 408 cairo_line_to(cr, offset.x() + content_size.width(), offset.y() - 1); | |
| 409 cairo_set_line_width(cr, 0.5); | |
| 410 cairo_set_source_rgb(cr, kDividerLineColorR, | |
| 411 kDividerLineColorG, kDividerLineColorB); | |
| 412 cairo_stroke(cr); | |
| 413 | |
| 414 cairo_destroy(cr); | |
| 415 | |
| 416 return FALSE; | |
| 417 } | |
| 418 | |
| 419 void BalloonViewImpl::OnOptionsMenuButton(GtkWidget* widget) { | |
| 420 options_menu_->PopupAsContext(gtk_get_current_event_time()); | |
| 421 } | |
| 422 | |
| 423 gboolean BalloonViewImpl::OnDestroy(GtkWidget* widget) { | |
| 424 frame_container_ = NULL; | |
| 425 Close(false); | |
| 426 return FALSE; // Propagate. | |
| 427 } | |
| OLD | NEW |