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.
5 #include "chrome/browser/ui/gtk/notifications/balloon_view_gtk.h"
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/notifications/balloon.h"
19 #include "chrome/browser/notifications/desktop_notification_service.h"
20 #include "chrome/browser/notifications/notification.h"
21 #include "chrome/browser/notifications/notification_options_menu_model.h"
22 #include "chrome/browser/profiles/profile.h"
23 #include "chrome/browser/themes/theme_service.h"
24 #include "chrome/browser/ui/browser_list.h"
25 #include "chrome/browser/ui/browser_window.h"
26 #include "chrome/browser/ui/gtk/custom_button.h"
27 #include "chrome/browser/ui/gtk/gtk_theme_service.h"
28 #include "chrome/browser/ui/gtk/gtk_util.h"
29 #include "chrome/browser/ui/gtk/menu_gtk.h"
30 #include "chrome/browser/ui/gtk/notifications/balloon_view_host_gtk.h"
31 #include "chrome/browser/ui/gtk/rounded_window.h"
32 #include "content/public/browser/notification_source.h"
33 #include "content/public/browser/render_view_host.h"
34 #include "content/public/browser/render_widget_host_view.h"
35 #include "content/public/browser/web_contents.h"
36 #include "extensions/browser/process_manager.h"
37 #include "extensions/common/extension.h"
38 #include "grit/generated_resources.h"
39 #include "grit/theme_resources.h"
40 #include "ui/base/gtk/gtk_hig_constants.h"
41 #include "ui/base/l10n/l10n_util.h"
42 #include "ui/base/resource/resource_bundle.h"
43 #include "ui/gfx/animation/slide_animation.h"
44 #include "ui/gfx/canvas.h"
45 #include "ui/gfx/insets.h"
46 #include "ui/gfx/native_widget_types.h"
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;
57 // Properties of the origin label.
58 const int kLeftLabelMargin
= 8;
60 // TODO(johnnyg): Add a shadow for the frame.
61 const int kLeftShadowWidth
= 0;
62 const int kRightShadowWidth
= 0;
63 const int kTopShadowWidth
= 0;
64 const int kBottomShadowWidth
= 0;
66 // Space in pixels between text and icon on the buttons.
67 const int kButtonSpacing
= 3;
69 // Number of characters to show in the origin label before ellipsis.
70 const int kOriginLabelCharacters
= 18;
72 // The shelf height for the system default font size. It is scaled
73 // with changes in the default font size.
74 const int kDefaultShelfHeight
= 25;
76 // The amount that the bubble collections class offsets from the side of the
78 const int kScreenBorder
= 5;
80 // Colors specified in various ways for different parts of the UI.
81 // These match the windows colors in balloon_view.cc
82 const char* kLabelColor
= "#7D7D7D";
83 const double kShelfBackgroundColorR
= 245.0 / 255.0;
84 const double kShelfBackgroundColorG
= 245.0 / 255.0;
85 const double kShelfBackgroundColorB
= 245.0 / 255.0;
86 const double kDividerLineColorR
= 180.0 / 255.0;
87 const double kDividerLineColorG
= 180.0 / 255.0;
88 const double kDividerLineColorB
= 180.0 / 255.0;
90 // Makes the website label relatively smaller to the base text size.
91 const char* kLabelMarkup
= "<span size=\"small\" color=\"%s\">%s</span>";
95 BalloonViewImpl::BalloonViewImpl(BalloonCollection
* collection
)
98 frame_container_(NULL
),
101 html_container_(NULL
),
102 menu_showing_(false),
103 pending_close_(false),
104 weak_factory_(this) {}
106 BalloonViewImpl::~BalloonViewImpl() {
107 if (frame_container_
) {
108 GtkWidget
* widget
= frame_container_
;
109 frame_container_
= NULL
;
110 gtk_widget_hide(widget
);
114 void BalloonViewImpl::Close(bool by_user
) {
115 // Delay a system-initiated close if the menu is showing.
116 if (!by_user
&& menu_showing_
) {
117 pending_close_
= true;
119 base::MessageLoop::current()->PostTask(
121 base::Bind(&BalloonViewImpl::DelayedClose
,
122 weak_factory_
.GetWeakPtr(),
127 gfx::Size
BalloonViewImpl::GetSize() const {
128 // BalloonView has no size if it hasn't been shown yet (which is when
133 // Although this may not be the instantaneous size of the balloon if
134 // called in the middle of an animation, it is the effective size that
135 // will result from the animation.
136 return gfx::Size(GetDesiredTotalWidth(), GetDesiredTotalHeight());
139 BalloonHost
* BalloonViewImpl::GetHost() const {
140 return html_contents_
.get();
143 void BalloonViewImpl::DelayedClose(bool by_user
) {
144 html_contents_
->Shutdown();
145 if (frame_container_
) {
146 // It's possible that |frame_container_| was destroyed before the
147 // BalloonViewImpl if our related browser window was closed first.
148 gtk_widget_hide(frame_container_
);
150 balloon_
->OnClose(by_user
);
153 void BalloonViewImpl::RepositionToBalloon() {
154 if (!frame_container_
) {
155 // No need to create a slide animation when this balloon is fading out.
161 // Create an amination from the current position to the desired one.
166 gtk_window_get_position(GTK_WINDOW(frame_container_
), &start_x
, &start_y
);
167 gtk_window_get_size(GTK_WINDOW(frame_container_
), &start_w
, &start_h
);
169 int end_x
= balloon_
->GetPosition().x();
170 int end_y
= balloon_
->GetPosition().y();
171 int end_w
= GetDesiredTotalWidth();
172 int end_h
= GetDesiredTotalHeight();
174 anim_frame_start_
= gfx::Rect(start_x
, start_y
, start_w
, start_h
);
175 anim_frame_end_
= gfx::Rect(end_x
, end_y
, end_w
, end_h
);
176 animation_
.reset(new gfx::SlideAnimation(this));
180 void BalloonViewImpl::AnimationProgressed(const gfx::Animation
* animation
) {
181 DCHECK_EQ(animation
, animation_
.get());
183 // Linear interpolation from start to end position.
184 double end
= animation
->GetCurrentValue();
185 double start
= 1.0 - end
;
187 gfx::Rect
frame_position(
188 static_cast<int>(start
* anim_frame_start_
.x() +
189 end
* anim_frame_end_
.x()),
190 static_cast<int>(start
* anim_frame_start_
.y() +
191 end
* anim_frame_end_
.y()),
192 static_cast<int>(start
* anim_frame_start_
.width() +
193 end
* anim_frame_end_
.width()),
194 static_cast<int>(start
* anim_frame_start_
.height() +
195 end
* anim_frame_end_
.height()));
196 gtk_window_resize(GTK_WINDOW(frame_container_
),
197 frame_position
.width(), frame_position
.height());
198 gtk_window_move(GTK_WINDOW(frame_container_
),
199 frame_position
.x(), frame_position
.y());
201 gfx::Rect contents_rect
= GetContentsRectangle();
202 html_contents_
->UpdateActualSize(contents_rect
.size());
205 void BalloonViewImpl::Show(Balloon
* balloon
) {
206 theme_service_
= GtkThemeService::GetFrom(balloon
->profile());
208 const std::string source_label_text
= l10n_util::GetStringFUTF8(
209 IDS_NOTIFICATION_BALLOON_SOURCE_LABEL
,
210 balloon
->notification().display_source());
211 const std::string options_text
=
212 l10n_util::GetStringUTF8(IDS_NOTIFICATION_OPTIONS_MENU_LABEL
);
213 const std::string dismiss_text
=
214 l10n_util::GetStringUTF8(IDS_NOTIFICATION_BALLOON_DISMISS_LABEL
);
217 frame_container_
= gtk_window_new(GTK_WINDOW_POPUP
);
219 g_signal_connect(frame_container_
, "expose-event",
220 G_CALLBACK(OnExposeThunk
), this);
221 g_signal_connect(frame_container_
, "destroy",
222 G_CALLBACK(OnDestroyThunk
), this);
224 // Construct the options menu.
225 options_menu_model_
.reset(new NotificationOptionsMenuModel(balloon_
));
226 options_menu_
.reset(new MenuGtk(this, options_menu_model_
.get()));
228 // Create a BalloonViewHost to host the HTML contents of this balloon.
229 html_contents_
.reset(new BalloonViewHost(balloon
));
230 html_contents_
->Init();
231 gfx::NativeView contents
= html_contents_
->native_view();
232 g_signal_connect_after(contents
, "expose-event",
233 G_CALLBACK(OnContentsExposeThunk
), this);
235 // Divide the frame vertically into the shelf and the content area.
236 GtkWidget
* vbox
= gtk_vbox_new(0, 0);
237 gtk_container_add(GTK_CONTAINER(frame_container_
), vbox
);
239 // Create the toolbar.
240 shelf_
= gtk_hbox_new(FALSE
, 0);
241 gtk_widget_set_size_request(GTK_WIDGET(shelf_
), -1, GetShelfHeight());
242 gtk_container_add(GTK_CONTAINER(vbox
), shelf_
);
244 // Create a label for the source of the notification and add it to the
246 GtkWidget
* source_label_
= gtk_label_new(NULL
);
247 char* markup
= g_markup_printf_escaped(kLabelMarkup
,
249 source_label_text
.c_str());
250 gtk_label_set_markup(GTK_LABEL(source_label_
), 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.5, 0, 0);
256 gtk_alignment_set_padding(GTK_ALIGNMENT(label_alignment
),
257 0, 0, kLeftLabelMargin
, 0);
258 gtk_container_add(GTK_CONTAINER(label_alignment
), source_label_
);
259 gtk_box_pack_start(GTK_BOX(shelf_
), label_alignment
, FALSE
, FALSE
, 0);
261 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
263 // Create a button to dismiss the balloon and add it to the toolbar.
264 close_button_
.reset(CustomDrawButton::CloseButtonBar(theme_service_
));
265 close_button_
->SetBackground(
267 rb
.GetImageNamed(IDR_CLOSE_1
).AsBitmap(),
268 rb
.GetImageNamed(IDR_CLOSE_1_MASK
).AsBitmap());
269 gtk_widget_set_tooltip_text(close_button_
->widget(), dismiss_text
.c_str());
270 g_signal_connect(close_button_
->widget(), "clicked",
271 G_CALLBACK(OnCloseButtonThunk
), this);
272 gtk_widget_set_can_focus(close_button_
->widget(), FALSE
);
273 GtkWidget
* close_alignment
= gtk_alignment_new(0.0, 0.5, 0, 0);
274 gtk_container_add(GTK_CONTAINER(close_alignment
), close_button_
->widget());
275 gtk_box_pack_end(GTK_BOX(shelf_
), close_alignment
, FALSE
, FALSE
,
278 // Create a button for showing the options menu, and add it to the toolbar.
279 options_menu_button_
.reset(new CustomDrawButton(IDR_BALLOON_WRENCH
,
280 IDR_BALLOON_WRENCH_P
,
281 IDR_BALLOON_WRENCH_H
,
283 gtk_widget_set_tooltip_text(options_menu_button_
->widget(),
284 options_text
.c_str());
285 g_signal_connect(options_menu_button_
->widget(), "button-press-event",
286 G_CALLBACK(OnOptionsMenuButtonThunk
), this);
287 gtk_widget_set_can_focus(options_menu_button_
->widget(), FALSE
);
288 GtkWidget
* options_alignment
= gtk_alignment_new(0.0, 0.5, 0, 0);
289 gtk_container_add(GTK_CONTAINER(options_alignment
),
290 options_menu_button_
->widget());
291 gtk_box_pack_end(GTK_BOX(shelf_
), options_alignment
, FALSE
, FALSE
, 0);
293 // Add main contents to bubble.
294 GtkWidget
* alignment
= gtk_alignment_new(0.0, 0.0, 1.0, 1.0);
295 gtk_alignment_set_padding(
296 GTK_ALIGNMENT(alignment
),
297 kTopMargin
, kBottomMargin
, kLeftMargin
, kRightMargin
);
298 gtk_widget_show_all(alignment
);
299 gtk_container_add(GTK_CONTAINER(alignment
), contents
);
300 gtk_container_add(GTK_CONTAINER(vbox
), alignment
);
301 gtk_widget_show_all(vbox
);
303 notification_registrar_
.Add(this, chrome::NOTIFICATION_BROWSER_THEME_CHANGED
,
304 content::Source
<ThemeService
>(theme_service_
));
306 // We don't do InitThemesFor() because it just forces a redraw.
307 gtk_util::ActAsRoundedWindow(frame_container_
, ui::kGdkBlack
, 3,
308 gtk_util::ROUNDED_ALL
,
309 gtk_util::BORDER_ALL
);
311 // Realize the frame container so we can do size calculations.
312 gtk_widget_realize(frame_container_
);
314 // Update to make sure we have everything sized properly and then move our
315 // window offscreen for its initial animation.
316 html_contents_
->UpdateActualSize(balloon_
->content_size());
318 gtk_window_get_size(GTK_WINDOW(frame_container_
), &window_width
, NULL
);
320 int pos_x
= gdk_screen_width() - window_width
- kScreenBorder
;
321 int pos_y
= gdk_screen_height();
322 gtk_window_move(GTK_WINDOW(frame_container_
), pos_x
, pos_y
);
323 balloon_
->SetPosition(gfx::Point(pos_x
, pos_y
), false);
324 gtk_widget_show_all(frame_container_
);
326 notification_registrar_
.Add(this,
327 chrome::NOTIFICATION_NOTIFY_BALLOON_DISCONNECTED
,
328 content::Source
<Balloon
>(balloon
));
331 void BalloonViewImpl::Update() {
332 DCHECK(html_contents_
.get()) << "BalloonView::Update called before Show";
333 if (!html_contents_
->web_contents())
335 html_contents_
->web_contents()->GetController().LoadURL(
336 balloon_
->notification().content_url(), content::Referrer(),
337 content::PAGE_TRANSITION_LINK
, std::string());
340 gfx::Point
BalloonViewImpl::GetContentsOffset() const {
341 return gfx::Point(kLeftShadowWidth
+ kLeftMargin
,
342 GetShelfHeight() + kTopShadowWidth
+ kTopMargin
);
345 int BalloonViewImpl::GetShelfHeight() const {
346 // TODO(johnnyg): add scaling here.
347 return kDefaultShelfHeight
;
350 int BalloonViewImpl::GetDesiredTotalWidth() const {
351 return balloon_
->content_size().width() +
352 kLeftMargin
+ kRightMargin
+ kLeftShadowWidth
+ kRightShadowWidth
;
355 int BalloonViewImpl::GetDesiredTotalHeight() const {
356 return balloon_
->content_size().height() +
357 kTopMargin
+ kBottomMargin
+ kTopShadowWidth
+ kBottomShadowWidth
+
361 gfx::Rect
BalloonViewImpl::GetContentsRectangle() const {
362 if (!frame_container_
)
365 gfx::Size content_size
= balloon_
->content_size();
366 gfx::Point offset
= GetContentsOffset();
368 gtk_window_get_position(GTK_WINDOW(frame_container_
), &x
, &y
);
369 return gfx::Rect(x
+ offset
.x(), y
+ offset
.y(),
370 content_size
.width(), content_size
.height());
373 void BalloonViewImpl::Observe(int type
,
374 const content::NotificationSource
& source
,
375 const content::NotificationDetails
& details
) {
376 if (type
== chrome::NOTIFICATION_NOTIFY_BALLOON_DISCONNECTED
) {
377 // If the renderer process attached to this balloon is disconnected
378 // (e.g., because of a crash), we want to close the balloon.
379 notification_registrar_
.Remove(this,
380 chrome::NOTIFICATION_NOTIFY_BALLOON_DISCONNECTED
,
381 content::Source
<Balloon
>(balloon_
));
383 } else if (type
== chrome::NOTIFICATION_BROWSER_THEME_CHANGED
) {
384 // Since all the buttons change their own properties, and our expose does
385 // all the real differences, we'll need a redraw.
386 gtk_widget_queue_draw(frame_container_
);
392 void BalloonViewImpl::OnCloseButton(GtkWidget
* widget
) {
396 // We draw black dots on the bottom left and right corners to fill in the
397 // border. Otherwise, the border has a gap because the sharp corners of the
398 // HTML view cut off the roundedness of the notification window.
399 gboolean
BalloonViewImpl::OnContentsExpose(GtkWidget
* sender
,
400 GdkEventExpose
* event
) {
401 TRACE_EVENT0("ui::gtk", "BalloonViewImpl::OnContentsExpose");
402 cairo_t
* cr
= gdk_cairo_create(gtk_widget_get_window(sender
));
403 gdk_cairo_rectangle(cr
, &event
->area
);
406 GtkAllocation allocation
;
407 gtk_widget_get_allocation(sender
, &allocation
);
409 // According to a discussion on a mailing list I found, these degenerate
410 // paths are the officially supported way to draw points in Cairo.
411 cairo_set_source_rgb(cr
, 0, 0, 0);
412 cairo_set_line_cap(cr
, CAIRO_LINE_CAP_ROUND
);
413 cairo_set_line_width(cr
, 1.0);
414 cairo_move_to(cr
, 0.5, allocation
.height
- 0.5);
415 cairo_close_path(cr
);
416 cairo_move_to(cr
, allocation
.width
- 0.5, allocation
.height
- 0.5);
417 cairo_close_path(cr
);
424 gboolean
BalloonViewImpl::OnExpose(GtkWidget
* sender
, GdkEventExpose
* event
) {
425 TRACE_EVENT0("ui::gtk", "BalloonViewImpl::OnExpose");
426 cairo_t
* cr
= gdk_cairo_create(gtk_widget_get_window(sender
));
427 gdk_cairo_rectangle(cr
, &event
->area
);
430 gfx::Size content_size
= balloon_
->content_size();
431 gfx::Point offset
= GetContentsOffset();
433 // Draw a background color behind the shelf.
434 cairo_set_source_rgb(cr
, kShelfBackgroundColorR
,
435 kShelfBackgroundColorG
, kShelfBackgroundColorB
);
436 cairo_rectangle(cr
, kLeftMargin
, kTopMargin
+ 0.5,
437 content_size
.width() - 0.5, GetShelfHeight());
440 // Now draw a one pixel line between content and shelf.
441 cairo_move_to(cr
, offset
.x(), offset
.y() - 1);
442 cairo_line_to(cr
, offset
.x() + content_size
.width(), offset
.y() - 1);
443 cairo_set_line_width(cr
, 0.5);
444 cairo_set_source_rgb(cr
, kDividerLineColorR
,
445 kDividerLineColorG
, kDividerLineColorB
);
453 void BalloonViewImpl::OnOptionsMenuButton(GtkWidget
* widget
,
454 GdkEventButton
* event
) {
455 menu_showing_
= true;
456 options_menu_
->PopupForWidget(widget
, event
->button
, event
->time
);
459 // Called when the menu stops showing.
460 void BalloonViewImpl::StoppedShowing() {
461 menu_showing_
= false;
462 if (pending_close_
) {
463 base::MessageLoop::current()->PostTask(
466 &BalloonViewImpl::DelayedClose
, weak_factory_
.GetWeakPtr(), false));
470 gboolean
BalloonViewImpl::OnDestroy(GtkWidget
* widget
) {
471 frame_container_
= NULL
;
473 return FALSE
; // Propagate.