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