OLD | NEW |
| (Empty) |
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/views/notifications/balloon_view_views.h" | |
6 | |
7 #include <algorithm> | |
8 #include <vector> | |
9 | |
10 #include "base/bind.h" | |
11 #include "base/message_loop/message_loop.h" | |
12 #include "base/strings/utf_string_conversions.h" | |
13 #include "chrome/browser/chrome_notification_types.h" | |
14 #include "chrome/browser/notifications/balloon_collection.h" | |
15 #include "chrome/browser/notifications/desktop_notification_service.h" | |
16 #include "chrome/browser/notifications/notification.h" | |
17 #include "chrome/browser/notifications/notification_options_menu_model.h" | |
18 #include "chrome/browser/ui/views/notifications/balloon_view_host.h" | |
19 #include "content/public/browser/notification_details.h" | |
20 #include "content/public/browser/notification_source.h" | |
21 #include "content/public/browser/notification_types.h" | |
22 #include "content/public/browser/render_view_host.h" | |
23 #include "content/public/browser/render_widget_host_view.h" | |
24 #include "content/public/browser/web_contents.h" | |
25 #include "grit/generated_resources.h" | |
26 #include "grit/theme_resources.h" | |
27 #include "ui/base/l10n/l10n_util.h" | |
28 #include "ui/base/resource/resource_bundle.h" | |
29 #include "ui/gfx/animation/slide_animation.h" | |
30 #include "ui/gfx/canvas.h" | |
31 #include "ui/gfx/native_widget_types.h" | |
32 #include "ui/gfx/path.h" | |
33 #include "ui/views/bubble/bubble_border.h" | |
34 #include "ui/views/controls/button/image_button.h" | |
35 #include "ui/views/controls/button/menu_button.h" | |
36 #include "ui/views/controls/button/text_button.h" | |
37 #include "ui/views/controls/label.h" | |
38 #include "ui/views/controls/menu/menu_item_view.h" | |
39 #include "ui/views/controls/menu/menu_runner.h" | |
40 #include "ui/views/controls/native/native_view_host.h" | |
41 #include "ui/views/widget/widget.h" | |
42 | |
43 namespace { | |
44 | |
45 const int kTopMargin = 2; | |
46 const int kBottomMargin = 0; | |
47 const int kLeftMargin = 4; | |
48 const int kRightMargin = 4; | |
49 | |
50 // Margin between various shelf buttons/label and the shelf border. | |
51 const int kShelfMargin = 2; | |
52 | |
53 // Spacing between the options and close buttons. | |
54 const int kOptionsDismissSpacing = 4; | |
55 | |
56 // Spacing between the options button and label text. | |
57 const int kLabelOptionsSpacing = 4; | |
58 | |
59 // Margin between shelf border and title label. | |
60 const int kLabelLeftMargin = 6; | |
61 | |
62 // Size of the drop shadow. The shadow is provided by BubbleBorder, | |
63 // not this class. | |
64 const int kLeftShadowWidth = 0; | |
65 const int kRightShadowWidth = 0; | |
66 const int kTopShadowWidth = 0; | |
67 const int kBottomShadowWidth = 6; | |
68 | |
69 // Optional animation. | |
70 const bool kAnimateEnabled = true; | |
71 | |
72 // Colors | |
73 const SkColor kControlBarBackgroundColor = SkColorSetRGB(245, 245, 245); | |
74 const SkColor kControlBarTextColor = SkColorSetRGB(125, 125, 125); | |
75 const SkColor kControlBarSeparatorLineColor = SkColorSetRGB(180, 180, 180); | |
76 | |
77 } // namespace | |
78 | |
79 // static | |
80 int BalloonView::GetHorizontalMargin() { | |
81 return kLeftMargin + kRightMargin + kLeftShadowWidth + kRightShadowWidth; | |
82 } | |
83 | |
84 BalloonViewImpl::BalloonViewImpl(BalloonCollection* collection) | |
85 : balloon_(NULL), | |
86 collection_(collection), | |
87 frame_container_(NULL), | |
88 html_container_(NULL), | |
89 close_button_(NULL), | |
90 options_menu_button_(NULL), | |
91 enable_web_ui_(false), | |
92 closed_by_user_(false), | |
93 closed_(false) { | |
94 // We're owned by Balloon and don't want to be deleted by our parent View. | |
95 set_owned_by_client(); | |
96 | |
97 SetBorder(scoped_ptr<views::Border>( | |
98 new views::BubbleBorder(views::BubbleBorder::FLOAT, | |
99 views::BubbleBorder::NO_SHADOW, | |
100 SK_ColorWHITE))); | |
101 } | |
102 | |
103 BalloonViewImpl::~BalloonViewImpl() { | |
104 } | |
105 | |
106 void BalloonViewImpl::Close(bool by_user) { | |
107 if (closed_) | |
108 return; | |
109 | |
110 closed_ = true; | |
111 animation_->Stop(); | |
112 html_contents_->Shutdown(); | |
113 // Detach contents from the widget before they close. | |
114 // This is necessary because a widget may be deleted | |
115 // after this when chrome is shutting down. | |
116 html_container_->GetRootView()->RemoveAllChildViews(true); | |
117 html_container_->Close(); | |
118 frame_container_->GetRootView()->RemoveAllChildViews(true); | |
119 frame_container_->Close(); | |
120 closed_by_user_ = by_user; | |
121 // |frame_container_->::Close()| is async. When processed it'll call back to | |
122 // DeleteDelegate() and we'll cleanup. | |
123 } | |
124 | |
125 gfx::Size BalloonViewImpl::GetSize() const { | |
126 // BalloonView has no size if it hasn't been shown yet (which is when | |
127 // balloon_ is set). | |
128 if (!balloon_) | |
129 return gfx::Size(0, 0); | |
130 | |
131 return gfx::Size(GetTotalWidth(), GetTotalHeight()); | |
132 } | |
133 | |
134 BalloonHost* BalloonViewImpl::GetHost() const { | |
135 return html_contents_.get(); | |
136 } | |
137 | |
138 void BalloonViewImpl::OnMenuButtonClicked(views::View* source, | |
139 const gfx::Point& point) { | |
140 CreateOptionsMenu(); | |
141 | |
142 menu_runner_.reset(new views::MenuRunner(options_menu_model_.get())); | |
143 | |
144 gfx::Point screen_location; | |
145 views::View::ConvertPointToScreen(options_menu_button_, &screen_location); | |
146 if (menu_runner_->RunMenuAt( | |
147 source->GetWidget()->GetTopLevelWidget(), | |
148 options_menu_button_, | |
149 gfx::Rect(screen_location, options_menu_button_->size()), | |
150 views::MenuItemView::TOPRIGHT, | |
151 ui::MENU_SOURCE_NONE, | |
152 views::MenuRunner::HAS_MNEMONICS) == views::MenuRunner::MENU_DELETED) | |
153 return; | |
154 } | |
155 | |
156 void BalloonViewImpl::OnDisplayChanged() { | |
157 collection_->DisplayChanged(); | |
158 } | |
159 | |
160 void BalloonViewImpl::OnWorkAreaChanged() { | |
161 collection_->DisplayChanged(); | |
162 } | |
163 | |
164 void BalloonViewImpl::DeleteDelegate() { | |
165 balloon_->OnClose(closed_by_user_); | |
166 } | |
167 | |
168 void BalloonViewImpl::ButtonPressed(views::Button* sender, const ui::Event&) { | |
169 // The only button currently is the close button. | |
170 DCHECK_EQ(close_button_, sender); | |
171 Close(true); | |
172 } | |
173 | |
174 gfx::Size BalloonViewImpl::GetPreferredSize() { | |
175 return gfx::Size(1000, 1000); | |
176 } | |
177 | |
178 void BalloonViewImpl::SizeContentsWindow() { | |
179 if (!html_container_ || !frame_container_) | |
180 return; | |
181 | |
182 gfx::Rect contents_rect = GetContentsRectangle(); | |
183 html_container_->SetBounds(contents_rect); | |
184 html_container_->StackAboveWidget(frame_container_); | |
185 | |
186 gfx::Path path; | |
187 GetContentsMask(contents_rect, &path); | |
188 html_container_->SetShape(path.CreateNativeRegion()); | |
189 | |
190 close_button_->SetBoundsRect(GetCloseButtonBounds()); | |
191 options_menu_button_->SetBoundsRect(GetOptionsButtonBounds()); | |
192 source_label_->SetBoundsRect(GetLabelBounds()); | |
193 } | |
194 | |
195 void BalloonViewImpl::RepositionToBalloon() { | |
196 if (closed_) | |
197 return; | |
198 | |
199 DCHECK(frame_container_); | |
200 DCHECK(html_container_); | |
201 DCHECK(balloon_); | |
202 | |
203 if (!kAnimateEnabled) { | |
204 frame_container_->SetBounds(GetBoundsForFrameContainer()); | |
205 gfx::Rect contents_rect = GetContentsRectangle(); | |
206 html_container_->SetBounds(contents_rect); | |
207 html_contents_->SetPreferredSize(contents_rect.size()); | |
208 content::RenderWidgetHostView* view = | |
209 html_contents_->web_contents()->GetRenderWidgetHostView(); | |
210 if (view) | |
211 view->SetSize(contents_rect.size()); | |
212 return; | |
213 } | |
214 | |
215 anim_frame_end_ = GetBoundsForFrameContainer(); | |
216 anim_frame_start_ = frame_container_->GetClientAreaBoundsInScreen(); | |
217 animation_.reset(new gfx::SlideAnimation(this)); | |
218 animation_->Show(); | |
219 } | |
220 | |
221 void BalloonViewImpl::Update() { | |
222 if (closed_) | |
223 return; | |
224 | |
225 // Tls might get called before html_contents_ is set in Show() if more than | |
226 // one update with the same replace_id occurs, or if an update occurs after | |
227 // the ballon has been closed (e.g. during shutdown) but before this has been | |
228 // destroyed. | |
229 if (!html_contents_.get() || !html_contents_->web_contents()) | |
230 return; | |
231 html_contents_->web_contents()->GetController().LoadURL( | |
232 balloon_->notification().content_url(), content::Referrer(), | |
233 content::PAGE_TRANSITION_LINK, std::string()); | |
234 } | |
235 | |
236 void BalloonViewImpl::AnimationProgressed(const gfx::Animation* animation) { | |
237 DCHECK_EQ(animation_.get(), animation); | |
238 | |
239 // Linear interpolation from start to end position. | |
240 gfx::Rect frame_position(animation_->CurrentValueBetween( | |
241 anim_frame_start_, anim_frame_end_)); | |
242 frame_container_->SetBounds(frame_position); | |
243 | |
244 gfx::Path path; | |
245 gfx::Rect contents_rect = GetContentsRectangle(); | |
246 html_container_->SetBounds(contents_rect); | |
247 GetContentsMask(contents_rect, &path); | |
248 html_container_->SetShape(path.CreateNativeRegion()); | |
249 | |
250 html_contents_->SetPreferredSize(contents_rect.size()); | |
251 content::RenderWidgetHostView* view = | |
252 html_contents_->web_contents()->GetRenderWidgetHostView(); | |
253 if (view) | |
254 view->SetSize(contents_rect.size()); | |
255 } | |
256 | |
257 gfx::Rect BalloonViewImpl::GetCloseButtonBounds() const { | |
258 gfx::Rect bounds(GetContentsBounds()); | |
259 bounds.set_height(GetShelfHeight()); | |
260 const gfx::Size& pref_size(close_button_->GetPreferredSize()); | |
261 bounds.Inset(bounds.width() - kShelfMargin - pref_size.width(), 0, | |
262 kShelfMargin, 0); | |
263 bounds.ClampToCenteredSize(pref_size); | |
264 return bounds; | |
265 } | |
266 | |
267 gfx::Rect BalloonViewImpl::GetOptionsButtonBounds() const { | |
268 gfx::Rect bounds(GetContentsBounds()); | |
269 bounds.set_height(GetShelfHeight()); | |
270 const gfx::Size& pref_size(options_menu_button_->GetPreferredSize()); | |
271 bounds.set_x(GetCloseButtonBounds().x() - kOptionsDismissSpacing - | |
272 pref_size.width()); | |
273 bounds.set_width(pref_size.width()); | |
274 bounds.ClampToCenteredSize(pref_size); | |
275 return bounds; | |
276 } | |
277 | |
278 gfx::Rect BalloonViewImpl::GetLabelBounds() const { | |
279 gfx::Rect bounds(GetContentsBounds()); | |
280 bounds.set_height(GetShelfHeight()); | |
281 gfx::Size pref_size(source_label_->GetPreferredSize()); | |
282 bounds.Inset(kLabelLeftMargin, 0, bounds.width() - | |
283 GetOptionsButtonBounds().x() + kLabelOptionsSpacing, 0); | |
284 pref_size.set_width(bounds.width()); | |
285 bounds.ClampToCenteredSize(pref_size); | |
286 return bounds; | |
287 } | |
288 | |
289 void BalloonViewImpl::Show(Balloon* balloon) { | |
290 if (closed_) | |
291 return; | |
292 | |
293 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); | |
294 | |
295 balloon_ = balloon; | |
296 | |
297 const base::string16 source_label_text = l10n_util::GetStringFUTF16( | |
298 IDS_NOTIFICATION_BALLOON_SOURCE_LABEL, | |
299 balloon->notification().display_source()); | |
300 | |
301 source_label_ = new views::Label(source_label_text); | |
302 AddChildView(source_label_); | |
303 options_menu_button_ = | |
304 new views::MenuButton(NULL, base::string16(), this, false); | |
305 AddChildView(options_menu_button_); | |
306 close_button_ = new views::ImageButton(this); | |
307 close_button_->SetTooltipText(l10n_util::GetStringUTF16( | |
308 IDS_NOTIFICATION_BALLOON_DISMISS_LABEL)); | |
309 AddChildView(close_button_); | |
310 | |
311 // We have to create two windows: one for the contents and one for the | |
312 // frame. Why? | |
313 // * The contents is an html window which cannot be a | |
314 // layered window (because it may have child windows for instance). | |
315 // * The frame is a layered window so that we can have nicely rounded | |
316 // corners using alpha blending (and we may do other alpha blending | |
317 // effects). | |
318 // Unfortunately, layered windows cannot have child windows. (Well, they can | |
319 // but the child windows don't render). | |
320 // | |
321 // We carefully keep these two windows in sync to present the illusion of | |
322 // one window to the user. | |
323 // | |
324 // We don't let the OS manage the RTL layout of these widgets, because | |
325 // this code is already taking care of correctly reversing the layout. | |
326 html_contents_.reset(new BalloonViewHost(balloon)); | |
327 html_contents_->SetPreferredSize(gfx::Size(10000, 10000)); | |
328 if (enable_web_ui_) | |
329 html_contents_->EnableWebUI(); | |
330 | |
331 html_container_ = new views::Widget; | |
332 views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP); | |
333 html_container_->Init(params); | |
334 html_container_->SetContentsView(html_contents_->view()); | |
335 | |
336 frame_container_ = new views::Widget; | |
337 params.delegate = this; | |
338 params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; | |
339 params.bounds = GetBoundsForFrameContainer(); | |
340 frame_container_->Init(params); | |
341 frame_container_->SetContentsView(this); | |
342 frame_container_->StackAboveWidget(html_container_); | |
343 | |
344 // GetContentsRectangle() is calculated relative to |frame_container_|. Make | |
345 // sure |frame_container_| has bounds before we ask for | |
346 // GetContentsRectangle(). | |
347 html_container_->SetBounds(GetContentsRectangle()); | |
348 | |
349 // SetAlwaysOnTop should be called after StackAboveWidget because otherwise | |
350 // the top-most flag will be removed. | |
351 html_container_->SetAlwaysOnTop(true); | |
352 frame_container_->SetAlwaysOnTop(true); | |
353 | |
354 close_button_->SetImage(views::CustomButton::STATE_NORMAL, | |
355 rb.GetImageSkiaNamed(IDR_CLOSE_1)); | |
356 close_button_->SetImage(views::CustomButton::STATE_HOVERED, | |
357 rb.GetImageSkiaNamed(IDR_CLOSE_1_H)); | |
358 close_button_->SetImage(views::CustomButton::STATE_PRESSED, | |
359 rb.GetImageSkiaNamed(IDR_CLOSE_1_P)); | |
360 close_button_->SetBoundsRect(GetCloseButtonBounds()); | |
361 close_button_->SetBackground(SK_ColorBLACK, | |
362 rb.GetImageSkiaNamed(IDR_CLOSE_1), | |
363 rb.GetImageSkiaNamed(IDR_CLOSE_1_MASK)); | |
364 | |
365 options_menu_button_->SetIcon(*rb.GetImageSkiaNamed(IDR_BALLOON_WRENCH)); | |
366 options_menu_button_->SetHoverIcon( | |
367 *rb.GetImageSkiaNamed(IDR_BALLOON_WRENCH_H)); | |
368 options_menu_button_->SetPushedIcon(*rb.GetImageSkiaNamed( | |
369 IDR_BALLOON_WRENCH_P)); | |
370 options_menu_button_->set_alignment(views::TextButton::ALIGN_CENTER); | |
371 options_menu_button_->SetBorder(views::Border::NullBorder()); | |
372 options_menu_button_->SetBoundsRect(GetOptionsButtonBounds()); | |
373 | |
374 source_label_->SetFontList(rb.GetFontList(ui::ResourceBundle::SmallFont)); | |
375 source_label_->SetBackgroundColor(kControlBarBackgroundColor); | |
376 source_label_->SetEnabledColor(kControlBarTextColor); | |
377 source_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT); | |
378 source_label_->SetBoundsRect(GetLabelBounds()); | |
379 | |
380 SizeContentsWindow(); | |
381 html_container_->Show(); | |
382 frame_container_->Show(); | |
383 | |
384 notification_registrar_.Add( | |
385 this, chrome::NOTIFICATION_NOTIFY_BALLOON_DISCONNECTED, | |
386 content::Source<Balloon>(balloon)); | |
387 } | |
388 | |
389 void BalloonViewImpl::CreateOptionsMenu() { | |
390 if (options_menu_model_.get()) | |
391 return; | |
392 options_menu_model_.reset(new NotificationOptionsMenuModel(balloon_)); | |
393 } | |
394 | |
395 void BalloonViewImpl::GetContentsMask(const gfx::Rect& rect, | |
396 gfx::Path* path) const { | |
397 // This rounds the corners, and we also cut out a circle for the close | |
398 // button, since we can't guarantee the ordering of two top-most windows. | |
399 SkScalar radius = SkIntToScalar(views::BubbleBorder::GetCornerRadius()); | |
400 SkScalar spline_radius = radius - | |
401 SkScalarMul(radius, (SK_ScalarSqrt2 - SK_Scalar1) * 4 / 3); | |
402 SkScalar left = SkIntToScalar(0); | |
403 SkScalar top = SkIntToScalar(0); | |
404 SkScalar right = SkIntToScalar(rect.width()); | |
405 SkScalar bottom = SkIntToScalar(rect.height()); | |
406 | |
407 path->moveTo(left, top); | |
408 path->lineTo(right, top); | |
409 path->lineTo(right, bottom - radius); | |
410 path->cubicTo(right, bottom - spline_radius, | |
411 right - spline_radius, bottom, | |
412 right - radius, bottom); | |
413 path->lineTo(left + radius, bottom); | |
414 path->cubicTo(left + spline_radius, bottom, | |
415 left, bottom - spline_radius, | |
416 left, bottom - radius); | |
417 path->lineTo(left, top); | |
418 path->close(); | |
419 } | |
420 | |
421 void BalloonViewImpl::GetFrameMask(const gfx::Rect& rect, | |
422 gfx::Path* path) const { | |
423 SkScalar radius = SkIntToScalar(views::BubbleBorder::GetCornerRadius()); | |
424 SkScalar spline_radius = radius - | |
425 SkScalarMul(radius, (SK_ScalarSqrt2 - SK_Scalar1) * 4 / 3); | |
426 SkScalar left = SkIntToScalar(rect.x()); | |
427 SkScalar top = SkIntToScalar(rect.y()); | |
428 SkScalar right = SkIntToScalar(rect.right()); | |
429 SkScalar bottom = SkIntToScalar(rect.bottom()); | |
430 | |
431 path->moveTo(left, bottom); | |
432 path->lineTo(left, top + radius); | |
433 path->cubicTo(left, top + spline_radius, | |
434 left + spline_radius, top, | |
435 left + radius, top); | |
436 path->lineTo(right - radius, top); | |
437 path->cubicTo(right - spline_radius, top, | |
438 right, top + spline_radius, | |
439 right, top + radius); | |
440 path->lineTo(right, bottom); | |
441 path->lineTo(left, bottom); | |
442 path->close(); | |
443 } | |
444 | |
445 gfx::Point BalloonViewImpl::GetContentsOffset() const { | |
446 return gfx::Point(kLeftShadowWidth + kLeftMargin, | |
447 kTopShadowWidth + kTopMargin); | |
448 } | |
449 | |
450 gfx::Rect BalloonViewImpl::GetBoundsForFrameContainer() const { | |
451 return gfx::Rect(balloon_->GetPosition().x(), balloon_->GetPosition().y(), | |
452 GetTotalWidth(), GetTotalHeight()); | |
453 } | |
454 | |
455 int BalloonViewImpl::GetShelfHeight() const { | |
456 // TODO(johnnyg): add scaling here. | |
457 int max_button_height = std::max(std::max( | |
458 close_button_->GetPreferredSize().height(), | |
459 options_menu_button_->GetPreferredSize().height()), | |
460 source_label_->GetPreferredSize().height()); | |
461 return max_button_height + kShelfMargin * 2; | |
462 } | |
463 | |
464 int BalloonViewImpl::GetBalloonFrameHeight() const { | |
465 return GetTotalHeight() - GetShelfHeight(); | |
466 } | |
467 | |
468 int BalloonViewImpl::GetTotalWidth() const { | |
469 return balloon_->content_size().width() + | |
470 kLeftMargin + kRightMargin + kLeftShadowWidth + kRightShadowWidth; | |
471 } | |
472 | |
473 int BalloonViewImpl::GetTotalHeight() const { | |
474 return balloon_->content_size().height() + | |
475 kTopMargin + kBottomMargin + kTopShadowWidth + kBottomShadowWidth + | |
476 GetShelfHeight(); | |
477 } | |
478 | |
479 gfx::Rect BalloonViewImpl::GetContentsRectangle() const { | |
480 if (!frame_container_) | |
481 return gfx::Rect(); | |
482 | |
483 gfx::Size content_size = balloon_->content_size(); | |
484 gfx::Point offset = GetContentsOffset(); | |
485 gfx::Rect frame_rect = frame_container_->GetWindowBoundsInScreen(); | |
486 return gfx::Rect(frame_rect.x() + offset.x(), | |
487 frame_rect.y() + GetShelfHeight() + offset.y(), | |
488 content_size.width(), | |
489 content_size.height()); | |
490 } | |
491 | |
492 void BalloonViewImpl::OnPaint(gfx::Canvas* canvas) { | |
493 DCHECK(canvas); | |
494 // Paint the menu bar area white, with proper rounded corners. | |
495 gfx::Path path; | |
496 gfx::Rect rect = GetContentsBounds(); | |
497 rect.set_height(GetShelfHeight()); | |
498 GetFrameMask(rect, &path); | |
499 | |
500 SkPaint paint; | |
501 paint.setAntiAlias(true); | |
502 paint.setColor(kControlBarBackgroundColor); | |
503 canvas->DrawPath(path, paint); | |
504 | |
505 // Draw a 1-pixel gray line between the content and the menu bar. | |
506 int line_width = GetTotalWidth() - kLeftMargin - kRightMargin; | |
507 canvas->FillRect(gfx::Rect(kLeftMargin, rect.bottom(), line_width, 1), | |
508 kControlBarSeparatorLineColor); | |
509 View::OnPaint(canvas); | |
510 OnPaintBorder(canvas); | |
511 } | |
512 | |
513 void BalloonViewImpl::OnBoundsChanged(const gfx::Rect& previous_bounds) { | |
514 SizeContentsWindow(); | |
515 } | |
516 | |
517 void BalloonViewImpl::Observe(int type, | |
518 const content::NotificationSource& source, | |
519 const content::NotificationDetails& details) { | |
520 if (type != chrome::NOTIFICATION_NOTIFY_BALLOON_DISCONNECTED) { | |
521 NOTREACHED(); | |
522 return; | |
523 } | |
524 | |
525 // If the renderer process attached to this balloon is disconnected | |
526 // (e.g., because of a crash), we want to close the balloon. | |
527 notification_registrar_.Remove( | |
528 this, chrome::NOTIFICATION_NOTIFY_BALLOON_DISCONNECTED, | |
529 content::Source<Balloon>(balloon_)); | |
530 Close(false); | |
531 } | |
OLD | NEW |