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/info_bubble_gtk.h" 6 7 #include <gdk/gdkkeysyms.h> 8 #include <vector> 9 10 #include "base/basictypes.h" 11 #include "base/logging.h" 12 #include "chrome/browser/ui/gtk/gtk_theme_service.h" 13 #include "chrome/browser/ui/gtk/gtk_util.h" 14 #include "chrome/browser/ui/gtk/info_bubble_accelerators_gtk.h" 15 #include "content/common/notification_service.h" 16 #include "ui/base/gtk/gtk_windowing.h" 17 #include "ui/gfx/gtk_util.h" 18 #include "ui/gfx/path.h" 19 #include "ui/gfx/rect.h" 20 21 namespace { 22 23 // The height of the arrow, and the width will be about twice the height. 24 const int kArrowSize = 8; 25 26 // Number of pixels to the middle of the arrow from the close edge of the 27 // window. 28 const int kArrowX = 18; 29 30 // Number of pixels between the tip of the arrow and the region we're 31 // pointing to. 32 const int kArrowToContentPadding = -4; 33 34 // We draw flat diagonal corners, each corner is an NxN square. 35 const int kCornerSize = 3; 36 37 // Margins around the content. 38 const int kTopMargin = kArrowSize + kCornerSize - 1; 39 const int kBottomMargin = kCornerSize - 1; 40 const int kLeftMargin = kCornerSize - 1; 41 const int kRightMargin = kCornerSize - 1; 42 43 const GdkColor kBackgroundColor = GDK_COLOR_RGB(0xff, 0xff, 0xff); 44 const GdkColor kFrameColor = GDK_COLOR_RGB(0x63, 0x63, 0x63); 45 46 } // namespace 47 48 // static 49 InfoBubbleGtk* InfoBubbleGtk::Show(GtkWidget* anchor_widget, 50 const gfx::Rect* rect, 51 GtkWidget* content, 52 ArrowLocationGtk arrow_location, 53 bool match_system_theme, 54 bool grab_input, 55 GtkThemeService* provider, 56 InfoBubbleGtkDelegate* delegate) { 57 InfoBubbleGtk* bubble = new InfoBubbleGtk(provider, match_system_theme); 58 bubble->Init(anchor_widget, rect, content, arrow_location, grab_input); 59 bubble->set_delegate(delegate); 60 return bubble; 61 } 62 63 InfoBubbleGtk::InfoBubbleGtk(GtkThemeService* provider, 64 bool match_system_theme) 65 : delegate_(NULL), 66 window_(NULL), 67 theme_service_(provider), 68 accel_group_(gtk_accel_group_new()), 69 toplevel_window_(NULL), 70 anchor_widget_(NULL), 71 mask_region_(NULL), 72 preferred_arrow_location_(ARROW_LOCATION_TOP_LEFT), 73 current_arrow_location_(ARROW_LOCATION_TOP_LEFT), 74 match_system_theme_(match_system_theme), 75 grab_input_(true), 76 closed_by_escape_(false) { 77 } 78 79 InfoBubbleGtk::~InfoBubbleGtk() { 80 // Notify the delegate that we're about to close. This gives the chance 81 // to save state / etc from the hosted widget before it's destroyed. 82 if (delegate_) 83 delegate_->InfoBubbleClosing(this, closed_by_escape_); 84 85 g_object_unref(accel_group_); 86 if (mask_region_) 87 gdk_region_destroy(mask_region_); 88 } 89 90 void InfoBubbleGtk::Init(GtkWidget* anchor_widget, 91 const gfx::Rect* rect, 92 GtkWidget* content, 93 ArrowLocationGtk arrow_location, 94 bool grab_input) { 95 // If there is a current grab widget (menu, other info bubble, etc.), hide it. 96 GtkWidget* current_grab_widget = gtk_grab_get_current(); 97 if (current_grab_widget) 98 gtk_widget_hide(current_grab_widget); 99 100 DCHECK(!window_); 101 anchor_widget_ = anchor_widget; 102 toplevel_window_ = GTK_WINDOW(gtk_widget_get_toplevel(anchor_widget_)); 103 DCHECK(GTK_WIDGET_TOPLEVEL(toplevel_window_)); 104 rect_ = rect ? *rect : gtk_util::WidgetBounds(anchor_widget); 105 preferred_arrow_location_ = arrow_location; 106 107 grab_input_ = grab_input; 108 // Using a TOPLEVEL window may cause placement issues with certain WMs but it 109 // is necessary to be able to focus the window. 110 window_ = gtk_window_new(grab_input ? GTK_WINDOW_POPUP : GTK_WINDOW_TOPLEVEL); 111 112 gtk_widget_set_app_paintable(window_, TRUE); 113 // Resizing is handled by the program, not user. 114 gtk_window_set_resizable(GTK_WINDOW(window_), FALSE); 115 116 // Attach all of the accelerators to the bubble. 117 InfoBubbleAcceleratorGtkList acceleratorList = 118 InfoBubbleAcceleratorsGtk::GetList(); 119 for (InfoBubbleAcceleratorGtkList::const_iterator iter = 120 acceleratorList.begin(); 121 iter != acceleratorList.end(); 122 ++iter) { 123 gtk_accel_group_connect(accel_group_, 124 iter->keyval, 125 iter->modifier_type, 126 GtkAccelFlags(0), 127 g_cclosure_new(G_CALLBACK(&OnGtkAcceleratorThunk), 128 this, 129 NULL)); 130 } 131 132 gtk_window_add_accel_group(GTK_WINDOW(window_), accel_group_); 133 134 GtkWidget* alignment = gtk_alignment_new(0.0, 0.0, 1.0, 1.0); 135 gtk_alignment_set_padding(GTK_ALIGNMENT(alignment), 136 kTopMargin, kBottomMargin, 137 kLeftMargin, kRightMargin); 138 139 gtk_container_add(GTK_CONTAINER(alignment), content); 140 gtk_container_add(GTK_CONTAINER(window_), alignment); 141 142 // GtkWidget only exposes the bitmap mask interface. Use GDK to more 143 // efficently mask a GdkRegion. Make sure the window is realized during 144 // OnSizeAllocate, so the mask can be applied to the GdkWindow. 145 gtk_widget_realize(window_); 146 147 UpdateArrowLocation(true); // Force move and reshape. 148 StackWindow(); 149 150 gtk_widget_add_events(window_, GDK_BUTTON_PRESS_MASK); 151 152 signals_.Connect(window_, "expose-event", G_CALLBACK(OnExposeThunk), this); 153 signals_.Connect(window_, "size-allocate", G_CALLBACK(OnSizeAllocateThunk), 154 this); 155 signals_.Connect(window_, "button-press-event", 156 G_CALLBACK(OnButtonPressThunk), this); 157 signals_.Connect(window_, "destroy", G_CALLBACK(OnDestroyThunk), this); 158 signals_.Connect(window_, "hide", G_CALLBACK(OnHideThunk), this); 159 160 // If the toplevel window is being used as the anchor, then the signals below 161 // are enough to keep us positioned correctly. 162 if (anchor_widget_ != GTK_WIDGET(toplevel_window_)) { 163 signals_.Connect(anchor_widget_, "size-allocate", 164 G_CALLBACK(OnAnchorAllocateThunk), this); 165 signals_.Connect(anchor_widget_, "destroy", 166 G_CALLBACK(gtk_widget_destroyed), &anchor_widget_); 167 } 168 169 signals_.Connect(toplevel_window_, "configure-event", 170 G_CALLBACK(OnToplevelConfigureThunk), this); 171 signals_.Connect(toplevel_window_, "unmap-event", 172 G_CALLBACK(OnToplevelUnmapThunk), this); 173 // Set |toplevel_window_| to NULL if it gets destroyed. 174 signals_.Connect(toplevel_window_, "destroy", 175 G_CALLBACK(gtk_widget_destroyed), &toplevel_window_); 176 177 gtk_widget_show_all(window_); 178 179 if (grab_input_) { 180 gtk_grab_add(window_); 181 GrabPointerAndKeyboard(); 182 } 183 184 registrar_.Add(this, NotificationType::BROWSER_THEME_CHANGED, 185 NotificationService::AllSources()); 186 theme_service_->InitThemesFor(this); 187 } 188 189 // NOTE: This seems a bit overcomplicated, but it requires a bunch of careful 190 // fudging to get the pixels rasterized exactly where we want them, the arrow to 191 // have a 1 pixel point, etc. 192 // TODO(deanm): Windows draws with Skia and uses some PNG images for the 193 // corners. This is a lot more work, but they get anti-aliasing. 194 // static 195 std::vector<GdkPoint> InfoBubbleGtk::MakeFramePolygonPoints( 196 ArrowLocationGtk arrow_location, 197 int width, 198 int height, 199 FrameType type) { 200 using gtk_util::MakeBidiGdkPoint; 201 std::vector<GdkPoint> points; 202 203 bool on_left = (arrow_location == ARROW_LOCATION_TOP_LEFT); 204 205 // If we're stroking the frame, we need to offset some of our points by 1 206 // pixel. We do this when we draw horizontal lines that are on the bottom or 207 // when we draw vertical lines that are closer to the end (where "end" is the 208 // right side for ARROW_LOCATION_TOP_LEFT). 209 int y_off = (type == FRAME_MASK) ? 0 : -1; 210 // We use this one for arrows located on the left. 211 int x_off_l = on_left ? y_off : 0; 212 // We use this one for RTL. 213 int x_off_r = !on_left ? -y_off : 0; 214 215 // Top left corner. 216 points.push_back(MakeBidiGdkPoint( 217 x_off_r, kArrowSize + kCornerSize - 1, width, on_left)); 218 points.push_back(MakeBidiGdkPoint( 219 kCornerSize + x_off_r - 1, kArrowSize, width, on_left)); 220 221 // The arrow. 222 points.push_back(MakeBidiGdkPoint( 223 kArrowX - kArrowSize + x_off_r, kArrowSize, width, on_left)); 224 points.push_back(MakeBidiGdkPoint( 225 kArrowX + x_off_r, 0, width, on_left)); 226 points.push_back(MakeBidiGdkPoint( 227 kArrowX + 1 + x_off_l, 0, width, on_left)); 228 points.push_back(MakeBidiGdkPoint( 229 kArrowX + kArrowSize + 1 + x_off_l, kArrowSize, width, on_left)); 230 231 // Top right corner. 232 points.push_back(MakeBidiGdkPoint( 233 width - kCornerSize + 1 + x_off_l, kArrowSize, width, on_left)); 234 points.push_back(MakeBidiGdkPoint( 235 width + x_off_l, kArrowSize + kCornerSize - 1, width, on_left)); 236 237 // Bottom right corner. 238 points.push_back(MakeBidiGdkPoint( 239 width + x_off_l, height - kCornerSize, width, on_left)); 240 points.push_back(MakeBidiGdkPoint( 241 width - kCornerSize + x_off_r, height + y_off, width, on_left)); 242 243 // Bottom left corner. 244 points.push_back(MakeBidiGdkPoint( 245 kCornerSize + x_off_l, height + y_off, width, on_left)); 246 points.push_back(MakeBidiGdkPoint( 247 x_off_r, height - kCornerSize, width, on_left)); 248 249 return points; 250 } 251 252 InfoBubbleGtk::ArrowLocationGtk InfoBubbleGtk::GetArrowLocation( 253 ArrowLocationGtk preferred_location, int arrow_x, int width) { 254 bool wants_left = (preferred_location == ARROW_LOCATION_TOP_LEFT); 255 int screen_width = gdk_screen_get_width(gdk_screen_get_default()); 256 257 bool left_is_onscreen = (arrow_x - kArrowX + width < screen_width); 258 bool right_is_onscreen = (arrow_x + kArrowX - width >= 0); 259 260 // Use the requested location if it fits onscreen, use whatever fits 261 // otherwise, and use the requested location if neither fits. 262 if (left_is_onscreen && (wants_left || !right_is_onscreen)) 263 return ARROW_LOCATION_TOP_LEFT; 264 if (right_is_onscreen && (!wants_left || !left_is_onscreen)) 265 return ARROW_LOCATION_TOP_RIGHT; 266 return (wants_left ? ARROW_LOCATION_TOP_LEFT : ARROW_LOCATION_TOP_RIGHT); 267 } 268 269 bool InfoBubbleGtk::UpdateArrowLocation(bool force_move_and_reshape) { 270 if (!toplevel_window_ || !anchor_widget_) 271 return false; 272 273 gint toplevel_x = 0, toplevel_y = 0; 274 gdk_window_get_position( 275 GTK_WIDGET(toplevel_window_)->window, &toplevel_x, &toplevel_y); 276 int offset_x, offset_y; 277 gtk_widget_translate_coordinates(anchor_widget_, GTK_WIDGET(toplevel_window_), 278 rect_.x(), rect_.y(), &offset_x, &offset_y); 279 280 ArrowLocationGtk old_location = current_arrow_location_; 281 current_arrow_location_ = GetArrowLocation( 282 preferred_arrow_location_, 283 toplevel_x + offset_x + (rect_.width() / 2), // arrow_x 284 window_->allocation.width); 285 286 if (force_move_and_reshape || current_arrow_location_ != old_location) { 287 UpdateWindowShape(); 288 MoveWindow(); 289 // We need to redraw the entire window to repaint its border. 290 gtk_widget_queue_draw(window_); 291 return true; 292 } 293 return false; 294 } 295 296 void InfoBubbleGtk::UpdateWindowShape() { 297 if (mask_region_) { 298 gdk_region_destroy(mask_region_); 299 mask_region_ = NULL; 300 } 301 std::vector<GdkPoint> points = MakeFramePolygonPoints( 302 current_arrow_location_, 303 window_->allocation.width, window_->allocation.height, 304 FRAME_MASK); 305 mask_region_ = gdk_region_polygon(&points[0], 306 points.size(), 307 GDK_EVEN_ODD_RULE); 308 gdk_window_shape_combine_region(window_->window, NULL, 0, 0); 309 gdk_window_shape_combine_region(window_->window, mask_region_, 0, 0); 310 } 311 312 void InfoBubbleGtk::MoveWindow() { 313 if (!toplevel_window_ || !anchor_widget_) 314 return; 315 316 gint toplevel_x = 0, toplevel_y = 0; 317 gdk_window_get_position( 318 GTK_WIDGET(toplevel_window_)->window, &toplevel_x, &toplevel_y); 319 320 int offset_x, offset_y; 321 gtk_widget_translate_coordinates(anchor_widget_, GTK_WIDGET(toplevel_window_), 322 rect_.x(), rect_.y(), &offset_x, &offset_y); 323 324 gint screen_x = 0; 325 if (current_arrow_location_ == ARROW_LOCATION_TOP_LEFT) { 326 screen_x = toplevel_x + offset_x + (rect_.width() / 2) - kArrowX; 327 } else if (current_arrow_location_ == ARROW_LOCATION_TOP_RIGHT) { 328 screen_x = toplevel_x + offset_x + (rect_.width() / 2) - 329 window_->allocation.width + kArrowX; 330 } else { 331 NOTREACHED(); 332 } 333 334 gint screen_y = toplevel_y + offset_y + rect_.height() + 335 kArrowToContentPadding; 336 337 gtk_window_move(GTK_WINDOW(window_), screen_x, screen_y); 338 } 339 340 void InfoBubbleGtk::StackWindow() { 341 // Stack our window directly above the toplevel window. 342 if (toplevel_window_) 343 ui::StackPopupWindow(window_, GTK_WIDGET(toplevel_window_)); 344 } 345 346 void InfoBubbleGtk::Observe(NotificationType type, 347 const NotificationSource& source, 348 const NotificationDetails& details) { 349 DCHECK_EQ(type.value, NotificationType::BROWSER_THEME_CHANGED); 350 if (theme_service_->UseGtkTheme() && match_system_theme_) { 351 gtk_widget_modify_bg(window_, GTK_STATE_NORMAL, NULL); 352 } else { 353 // Set the background color, so we don't need to paint it manually. 354 gtk_widget_modify_bg(window_, GTK_STATE_NORMAL, &kBackgroundColor); 355 } 356 } 357 358 void InfoBubbleGtk::HandlePointerAndKeyboardUngrabbedByContent() { 359 if (grab_input_) 360 GrabPointerAndKeyboard(); 361 } 362 363 void InfoBubbleGtk::Close() { 364 // We don't need to ungrab the pointer or keyboard here; the X server will 365 // automatically do that when we destroy our window. 366 DCHECK(window_); 367 gtk_widget_destroy(window_); 368 // |this| has been deleted, see OnDestroy. 369 } 370 371 void InfoBubbleGtk::GrabPointerAndKeyboard() { 372 // Install X pointer and keyboard grabs to make sure that we have the focus 373 // and get all mouse and keyboard events until we're closed. 374 GdkGrabStatus pointer_grab_status = 375 gdk_pointer_grab(window_->window, 376 TRUE, // owner_events 377 GDK_BUTTON_PRESS_MASK, // event_mask 378 NULL, // confine_to 379 NULL, // cursor 380 GDK_CURRENT_TIME); 381 if (pointer_grab_status != GDK_GRAB_SUCCESS) { 382 // This will fail if someone else already has the pointer grabbed, but 383 // there's not really anything we can do about that. 384 DLOG(ERROR) << "Unable to grab pointer (status=" 385 << pointer_grab_status << ")"; 386 } 387 GdkGrabStatus keyboard_grab_status = 388 gdk_keyboard_grab(window_->window, 389 FALSE, // owner_events 390 GDK_CURRENT_TIME); 391 if (keyboard_grab_status != GDK_GRAB_SUCCESS) { 392 DLOG(ERROR) << "Unable to grab keyboard (status=" 393 << keyboard_grab_status << ")"; 394 } 395 } 396 397 gboolean InfoBubbleGtk::OnGtkAccelerator(GtkAccelGroup* group, 398 GObject* acceleratable, 399 guint keyval, 400 GdkModifierType modifier) { 401 GdkEventKey msg; 402 GdkKeymapKey* keys; 403 gint n_keys; 404 405 switch (keyval) { 406 case GDK_Escape: 407 // Close on Esc and trap the accelerator 408 closed_by_escape_ = true; 409 Close(); 410 return TRUE; 411 case GDK_w: 412 // Close on C-w and forward the accelerator 413 if (modifier & GDK_CONTROL_MASK) { 414 Close(); 415 } 416 break; 417 default: 418 return FALSE; 419 } 420 421 gdk_keymap_get_entries_for_keyval(NULL, 422 keyval, 423 &keys, 424 &n_keys); 425 if (n_keys) { 426 // Forward the accelerator to root window the bubble is anchored 427 // to for further processing 428 msg.type = GDK_KEY_PRESS; 429 msg.window = GTK_WIDGET(toplevel_window_)->window; 430 msg.send_event = TRUE; 431 msg.time = GDK_CURRENT_TIME; 432 msg.state = modifier | GDK_MOD2_MASK; 433 msg.keyval = keyval; 434 // length and string are deprecated and thus zeroed out 435 msg.length = 0; 436 msg.string = NULL; 437 msg.hardware_keycode = keys[0].keycode; 438 msg.group = keys[0].group; 439 msg.is_modifier = 0; 440 441 g_free(keys); 442 443 gtk_main_do_event(reinterpret_cast<GdkEvent*>(&msg)); 444 } else { 445 // This means that there isn't a h/w code for the keyval in the 446 // current keymap, which is weird but possible if the keymap just 447 // changed. This isn't a critical error, but might be indicative 448 // of something off if it happens regularly. 449 DLOG(WARNING) << "Found no keys for value " << keyval; 450 } 451 return TRUE; 452 } 453 454 gboolean InfoBubbleGtk::OnExpose(GtkWidget* widget, GdkEventExpose* expose) { 455 GdkDrawable* drawable = GDK_DRAWABLE(window_->window); 456 GdkGC* gc = gdk_gc_new(drawable); 457 gdk_gc_set_rgb_fg_color(gc, &kFrameColor); 458 459 // Stroke the frame border. 460 std::vector<GdkPoint> points = MakeFramePolygonPoints( 461 current_arrow_location_, 462 window_->allocation.width, window_->allocation.height, 463 FRAME_STROKE); 464 gdk_draw_polygon(drawable, gc, FALSE, &points[0], points.size()); 465 466 g_object_unref(gc); 467 return FALSE; // Propagate so our children paint, etc. 468 } 469 470 // When our size is initially allocated or changed, we need to recompute 471 // and apply our shape mask region. 472 void InfoBubbleGtk::OnSizeAllocate(GtkWidget* widget, 473 GtkAllocation* allocation) { 474 if (!UpdateArrowLocation(false)) { 475 UpdateWindowShape(); 476 if (current_arrow_location_ == ARROW_LOCATION_TOP_RIGHT) 477 MoveWindow(); 478 } 479 } 480 481 gboolean InfoBubbleGtk::OnButtonPress(GtkWidget* widget, 482 GdkEventButton* event) { 483 // If we got a click in our own window, that's okay (we need to additionally 484 // check that it falls within our bounds, since we've grabbed the pointer and 485 // some events that actually occurred in other windows will be reported with 486 // respect to our window). 487 if (event->window == window_->window && 488 (mask_region_ && gdk_region_point_in(mask_region_, event->x, event->y))) { 489 return FALSE; // Propagate. 490 } 491 492 // Our content widget got a click. 493 if (event->window != window_->window && 494 gdk_window_get_toplevel(event->window) == window_->window) { 495 return FALSE; 496 } 497 498 if (grab_input_) { 499 // Otherwise we had a click outside of our window, close ourself. 500 Close(); 501 return TRUE; 502 } 503 504 return FALSE; 505 } 506 507 gboolean InfoBubbleGtk::OnDestroy(GtkWidget* widget) { 508 // We are self deleting, we have a destroy signal setup to catch when we 509 // destroy the widget manually, or the window was closed via X. This will 510 // delete the InfoBubbleGtk object. 511 delete this; 512 return FALSE; // Propagate. 513 } 514 515 void InfoBubbleGtk::OnHide(GtkWidget* widget) { 516 gtk_widget_destroy(widget); 517 } 518 519 gboolean InfoBubbleGtk::OnToplevelConfigure(GtkWidget* widget, 520 GdkEventConfigure* event) { 521 if (!UpdateArrowLocation(false)) 522 MoveWindow(); 523 StackWindow(); 524 return FALSE; 525 } 526 527 gboolean InfoBubbleGtk::OnToplevelUnmap(GtkWidget* widget, GdkEvent* event) { 528 Close(); 529 return FALSE; 530 } 531 532 void InfoBubbleGtk::OnAnchorAllocate(GtkWidget* widget, 533 GtkAllocation* allocation) { 534 if (!UpdateArrowLocation(false)) 535 MoveWindow(); 536 } 537