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/gtk/bubble/bubble_gtk.h" 6 7 #include <gdk/gdkkeysyms.h> 8 9 #include "base/bind.h" 10 #include "base/i18n/rtl.h" 11 #include "base/message_loop/message_loop.h" 12 #include "chrome/browser/chrome_notification_types.h" 13 #include "chrome/browser/ui/gtk/bubble/bubble_accelerators_gtk.h" 14 #include "chrome/browser/ui/gtk/gtk_theme_service.h" 15 #include "chrome/browser/ui/gtk/gtk_util.h" 16 #include "content/public/browser/notification_source.h" 17 #include "ui/base/gtk/gtk_hig_constants.h" 18 #include "ui/base/gtk/gtk_windowing.h" 19 #include "ui/gfx/gtk_compat.h" 20 #include "ui/gfx/gtk_util.h" 21 #include "ui/gfx/path.h" 22 #include "ui/gfx/rect.h" 23 24 namespace { 25 26 // The height of the arrow, and the width will be about twice the height. 27 const int kArrowSize = 8; 28 29 // Number of pixels to the middle of the arrow from the close edge of the 30 // window. 31 const int kArrowX = 18; 32 33 // Number of pixels between the tip of the arrow and the region we're 34 // pointing to. 35 const int kArrowToContentPadding = -4; 36 37 // We draw flat diagonal corners, each corner is an NxN square. 38 const int kCornerSize = 3; 39 40 // The amount of padding (in pixels) from the top of |toplevel_window_| to the 41 // top of |window_| when fixed positioning is used. 42 const int kFixedPositionPaddingEnd = 10; 43 const int kFixedPositionPaddingTop = 5; 44 45 const GdkColor kBackgroundColor = GDK_COLOR_RGB(0xff, 0xff, 0xff); 46 const GdkColor kFrameColor = GDK_COLOR_RGB(0x63, 0x63, 0x63); 47 48 // Helper functions that encapsulate arrow locations. 49 bool HasArrow(BubbleGtk::FrameStyle frame_style) { 50 return frame_style != BubbleGtk::FLOAT_BELOW_RECT && 51 frame_style != BubbleGtk::CENTER_OVER_RECT && 52 frame_style != BubbleGtk::FIXED_TOP_LEFT && 53 frame_style != BubbleGtk::FIXED_TOP_RIGHT; 54 } 55 56 bool IsArrowLeft(BubbleGtk::FrameStyle frame_style) { 57 return frame_style == BubbleGtk::ANCHOR_TOP_LEFT || 58 frame_style == BubbleGtk::ANCHOR_BOTTOM_LEFT; 59 } 60 61 bool IsArrowMiddle(BubbleGtk::FrameStyle frame_style) { 62 return frame_style == BubbleGtk::ANCHOR_TOP_MIDDLE || 63 frame_style == BubbleGtk::ANCHOR_BOTTOM_MIDDLE; 64 } 65 66 bool IsArrowRight(BubbleGtk::FrameStyle frame_style) { 67 return frame_style == BubbleGtk::ANCHOR_TOP_RIGHT || 68 frame_style == BubbleGtk::ANCHOR_BOTTOM_RIGHT; 69 } 70 71 bool IsArrowTop(BubbleGtk::FrameStyle frame_style) { 72 return frame_style == BubbleGtk::ANCHOR_TOP_LEFT || 73 frame_style == BubbleGtk::ANCHOR_TOP_MIDDLE || 74 frame_style == BubbleGtk::ANCHOR_TOP_RIGHT; 75 } 76 77 bool IsArrowBottom(BubbleGtk::FrameStyle frame_style) { 78 return frame_style == BubbleGtk::ANCHOR_BOTTOM_LEFT || 79 frame_style == BubbleGtk::ANCHOR_BOTTOM_MIDDLE || 80 frame_style == BubbleGtk::ANCHOR_BOTTOM_RIGHT; 81 } 82 83 bool IsFixed(BubbleGtk::FrameStyle frame_style) { 84 return frame_style == BubbleGtk::FIXED_TOP_LEFT || 85 frame_style == BubbleGtk::FIXED_TOP_RIGHT; 86 } 87 88 BubbleGtk::FrameStyle AdjustFrameStyleForLocale( 89 BubbleGtk::FrameStyle frame_style) { 90 // Only RTL requires more work. 91 if (!base::i18n::IsRTL()) 92 return frame_style; 93 94 switch (frame_style) { 95 // These don't flip. 96 case BubbleGtk::ANCHOR_TOP_MIDDLE: 97 case BubbleGtk::ANCHOR_BOTTOM_MIDDLE: 98 case BubbleGtk::FLOAT_BELOW_RECT: 99 case BubbleGtk::CENTER_OVER_RECT: 100 return frame_style; 101 102 // These do flip. 103 case BubbleGtk::ANCHOR_TOP_LEFT: 104 return BubbleGtk::ANCHOR_TOP_RIGHT; 105 106 case BubbleGtk::ANCHOR_TOP_RIGHT: 107 return BubbleGtk::ANCHOR_TOP_LEFT; 108 109 case BubbleGtk::ANCHOR_BOTTOM_LEFT: 110 return BubbleGtk::ANCHOR_BOTTOM_RIGHT; 111 112 case BubbleGtk::ANCHOR_BOTTOM_RIGHT: 113 return BubbleGtk::ANCHOR_BOTTOM_LEFT; 114 115 case BubbleGtk::FIXED_TOP_LEFT: 116 return BubbleGtk::FIXED_TOP_RIGHT; 117 118 case BubbleGtk::FIXED_TOP_RIGHT: 119 return BubbleGtk::FIXED_TOP_LEFT; 120 } 121 122 NOTREACHED(); 123 return BubbleGtk::ANCHOR_TOP_LEFT; 124 } 125 126 } // namespace 127 128 // static 129 BubbleGtk* BubbleGtk::Show(GtkWidget* anchor_widget, 130 const gfx::Rect* rect, 131 GtkWidget* content, 132 FrameStyle frame_style, 133 int attribute_flags, 134 GtkThemeService* provider, 135 BubbleDelegateGtk* delegate) { 136 BubbleGtk* bubble = new BubbleGtk(provider, 137 AdjustFrameStyleForLocale(frame_style), 138 attribute_flags); 139 bubble->Init(anchor_widget, rect, content, attribute_flags); 140 bubble->set_delegate(delegate); 141 return bubble; 142 } 143 144 BubbleGtk::BubbleGtk(GtkThemeService* provider, 145 FrameStyle frame_style, 146 int attribute_flags) 147 : delegate_(NULL), 148 window_(NULL), 149 theme_service_(provider), 150 accel_group_(gtk_accel_group_new()), 151 toplevel_window_(NULL), 152 anchor_widget_(NULL), 153 mask_region_(NULL), 154 requested_frame_style_(frame_style), 155 actual_frame_style_(ANCHOR_TOP_LEFT), 156 match_system_theme_(attribute_flags & MATCH_SYSTEM_THEME), 157 grab_input_(attribute_flags & GRAB_INPUT), 158 closed_by_escape_(false), 159 weak_ptr_factory_(this) {} 160 161 BubbleGtk::~BubbleGtk() { 162 // Notify the delegate that we're about to close. This gives the chance 163 // to save state / etc from the hosted widget before it's destroyed. 164 if (delegate_) 165 delegate_->BubbleClosing(this, closed_by_escape_); 166 167 g_object_unref(accel_group_); 168 if (mask_region_) 169 gdk_region_destroy(mask_region_); 170 } 171 172 void BubbleGtk::Init(GtkWidget* anchor_widget, 173 const gfx::Rect* rect, 174 GtkWidget* content, 175 int attribute_flags) { 176 // If there is a current grab widget (menu, other bubble, etc.), hide it. 177 GtkWidget* current_grab_widget = gtk_grab_get_current(); 178 if (current_grab_widget) 179 gtk_widget_hide(current_grab_widget); 180 181 DCHECK(!window_); 182 anchor_widget_ = anchor_widget; 183 toplevel_window_ = gtk_widget_get_toplevel(anchor_widget_); 184 DCHECK(gtk_widget_is_toplevel(toplevel_window_)); 185 rect_ = rect ? *rect : gtk_util::WidgetBounds(anchor_widget); 186 187 // Using a TOPLEVEL window may cause placement issues with certain WMs but it 188 // is necessary to be able to focus the window. 189 window_ = gtk_window_new(attribute_flags & POPUP_WINDOW ? 190 GTK_WINDOW_POPUP : GTK_WINDOW_TOPLEVEL); 191 192 gtk_widget_set_app_paintable(window_, TRUE); 193 // Resizing is handled by the program, not user. 194 gtk_window_set_resizable(GTK_WINDOW(window_), FALSE); 195 196 if (!(attribute_flags & NO_ACCELERATORS)) { 197 // Attach all of the accelerators to the bubble. 198 for (BubbleAcceleratorsGtk::const_iterator 199 i(BubbleAcceleratorsGtk::begin()); 200 i != BubbleAcceleratorsGtk::end(); 201 ++i) { 202 gtk_accel_group_connect(accel_group_, 203 i->keyval, 204 i->modifier_type, 205 GtkAccelFlags(0), 206 g_cclosure_new(G_CALLBACK(&OnGtkAcceleratorThunk), 207 this, 208 NULL)); 209 } 210 gtk_window_add_accel_group(GTK_WINDOW(window_), accel_group_); 211 } 212 213 // |requested_frame_style_| is used instead of |actual_frame_style_| here 214 // because |actual_frame_style_| is only correct after calling 215 // |UpdateFrameStyle()|. Unfortunately, |UpdateFrameStyle()| requires knowing 216 // the size of |window_| (which happens later on). 217 int arrow_padding = HasArrow(requested_frame_style_) ? kArrowSize : 0; 218 GtkWidget* alignment = gtk_alignment_new(0.0, 0.0, 1.0, 1.0); 219 gtk_alignment_set_padding(GTK_ALIGNMENT(alignment), arrow_padding, 0, 0, 0); 220 221 gtk_container_add(GTK_CONTAINER(alignment), content); 222 gtk_container_add(GTK_CONTAINER(window_), alignment); 223 224 // GtkWidget only exposes the bitmap mask interface. Use GDK to more 225 // efficently mask a GdkRegion. Make sure the window is realized during 226 // OnSizeAllocate, so the mask can be applied to the GdkWindow. 227 gtk_widget_realize(window_); 228 229 UpdateFrameStyle(true); // Force move and reshape. 230 StackWindow(); 231 232 gtk_widget_add_events(window_, GDK_BUTTON_PRESS_MASK); 233 234 // Connect during the bubbling phase so the border is always on top. 235 signals_.ConnectAfter(window_, "expose-event", 236 G_CALLBACK(OnExposeThunk), this); 237 signals_.Connect(window_, "size-allocate", G_CALLBACK(OnSizeAllocateThunk), 238 this); 239 signals_.Connect(window_, "button-press-event", 240 G_CALLBACK(OnButtonPressThunk), this); 241 signals_.Connect(window_, "destroy", G_CALLBACK(OnDestroyThunk), this); 242 signals_.Connect(window_, "hide", G_CALLBACK(OnHideThunk), this); 243 if (grab_input_) { 244 signals_.Connect(window_, "grab-broken-event", 245 G_CALLBACK(OnGrabBrokenThunk), this); 246 } 247 248 signals_.Connect(anchor_widget_, "destroy", 249 G_CALLBACK(OnAnchorDestroyThunk), this); 250 // If the toplevel window is being used as the anchor, then the signals below 251 // are enough to keep us positioned correctly. 252 if (anchor_widget_ != toplevel_window_) { 253 signals_.Connect(anchor_widget_, "size-allocate", 254 G_CALLBACK(OnAnchorAllocateThunk), this); 255 } 256 257 signals_.Connect(toplevel_window_, "configure-event", 258 G_CALLBACK(OnToplevelConfigureThunk), this); 259 signals_.Connect(toplevel_window_, "unmap-event", 260 G_CALLBACK(OnToplevelUnmapThunk), this); 261 262 gtk_widget_show_all(window_); 263 264 if (grab_input_) 265 gtk_grab_add(window_); 266 GrabPointerAndKeyboard(); 267 268 registrar_.Add(this, chrome::NOTIFICATION_BROWSER_THEME_CHANGED, 269 content::Source<ThemeService>(theme_service_)); 270 theme_service_->InitThemesFor(this); 271 } 272 273 // NOTE: This seems a bit overcomplicated, but it requires a bunch of careful 274 // fudging to get the pixels rasterized exactly where we want them, the arrow to 275 // have a 1 pixel point, etc. 276 // TODO(deanm): Windows draws with Skia and uses some PNG images for the 277 // corners. This is a lot more work, but they get anti-aliasing. 278 // static 279 std::vector<GdkPoint> BubbleGtk::MakeFramePolygonPoints( 280 FrameStyle frame_style, 281 int width, 282 int height, 283 FrameType type) { 284 using gtk_util::MakeBidiGdkPoint; 285 std::vector<GdkPoint> points; 286 287 int top_arrow_size = IsArrowTop(frame_style) ? kArrowSize : 0; 288 int bottom_arrow_size = IsArrowBottom(frame_style) ? kArrowSize : 0; 289 bool on_left = IsArrowLeft(frame_style); 290 291 // If we're stroking the frame, we need to offset some of our points by 1 292 // pixel. We do this when we draw horizontal lines that are on the bottom or 293 // when we draw vertical lines that are closer to the end (where "end" is the 294 // right side for ANCHOR_TOP_LEFT). 295 int y_off = type == FRAME_MASK ? 0 : -1; 296 // We use this one for arrows located on the left. 297 int x_off_l = on_left ? y_off : 0; 298 // We use this one for RTL. 299 int x_off_r = !on_left ? -y_off : 0; 300 301 // Top left corner. 302 points.push_back(MakeBidiGdkPoint( 303 x_off_r, top_arrow_size + kCornerSize - 1, width, on_left)); 304 points.push_back(MakeBidiGdkPoint( 305 kCornerSize + x_off_r - 1, top_arrow_size, width, on_left)); 306 307 // The top arrow. 308 if (top_arrow_size) { 309 int arrow_x = frame_style == ANCHOR_TOP_MIDDLE ? width / 2 : kArrowX; 310 points.push_back(MakeBidiGdkPoint( 311 arrow_x - top_arrow_size + x_off_r, top_arrow_size, width, on_left)); 312 points.push_back(MakeBidiGdkPoint( 313 arrow_x + x_off_r, 0, width, on_left)); 314 points.push_back(MakeBidiGdkPoint( 315 arrow_x + 1 + x_off_l, 0, width, on_left)); 316 points.push_back(MakeBidiGdkPoint( 317 arrow_x + top_arrow_size + 1 + x_off_l, top_arrow_size, 318 width, on_left)); 319 } 320 321 // Top right corner. 322 points.push_back(MakeBidiGdkPoint( 323 width - kCornerSize + 1 + x_off_l, top_arrow_size, width, on_left)); 324 points.push_back(MakeBidiGdkPoint( 325 width + x_off_l, top_arrow_size + kCornerSize - 1, width, on_left)); 326 327 // Bottom right corner. 328 points.push_back(MakeBidiGdkPoint( 329 width + x_off_l, height - bottom_arrow_size - kCornerSize, 330 width, on_left)); 331 points.push_back(MakeBidiGdkPoint( 332 width - kCornerSize + x_off_r, height - bottom_arrow_size + y_off, 333 width, on_left)); 334 335 // The bottom arrow. 336 if (bottom_arrow_size) { 337 int arrow_x = frame_style == ANCHOR_BOTTOM_MIDDLE ? 338 width / 2 : kArrowX; 339 points.push_back(MakeBidiGdkPoint( 340 arrow_x + bottom_arrow_size + 1 + x_off_l, 341 height - bottom_arrow_size + y_off, 342 width, 343 on_left)); 344 points.push_back(MakeBidiGdkPoint( 345 arrow_x + 1 + x_off_l, height + y_off, width, on_left)); 346 points.push_back(MakeBidiGdkPoint( 347 arrow_x + x_off_r, height + y_off, width, on_left)); 348 points.push_back(MakeBidiGdkPoint( 349 arrow_x - bottom_arrow_size + x_off_r, 350 height - bottom_arrow_size + y_off, 351 width, 352 on_left)); 353 } 354 355 // Bottom left corner. 356 points.push_back(MakeBidiGdkPoint( 357 kCornerSize + x_off_l, height -bottom_arrow_size + y_off, 358 width, on_left)); 359 points.push_back(MakeBidiGdkPoint( 360 x_off_r, height - bottom_arrow_size - kCornerSize, width, on_left)); 361 362 return points; 363 } 364 365 BubbleGtk::FrameStyle BubbleGtk::GetAllowedFrameStyle( 366 FrameStyle preferred_style, 367 int arrow_x, 368 int arrow_y, 369 int width, 370 int height) { 371 if (IsFixed(preferred_style)) 372 return preferred_style; 373 374 const int screen_width = gdk_screen_get_width(gdk_screen_get_default()); 375 const int screen_height = gdk_screen_get_height(gdk_screen_get_default()); 376 377 // Choose whether to show the bubble above or below the specified location. 378 const bool prefer_top_arrow = IsArrowTop(preferred_style) || 379 preferred_style == FLOAT_BELOW_RECT; 380 // The bleed measures the amount of bubble that would be shown offscreen. 381 const int top_arrow_bleed = 382 std::max(height + kArrowSize + arrow_y - screen_height, 0); 383 const int bottom_arrow_bleed = std::max(height + kArrowSize - arrow_y, 0); 384 385 FrameStyle frame_style_none = FLOAT_BELOW_RECT; 386 FrameStyle frame_style_left = ANCHOR_TOP_LEFT; 387 FrameStyle frame_style_middle = ANCHOR_TOP_MIDDLE; 388 FrameStyle frame_style_right = ANCHOR_TOP_RIGHT; 389 if ((prefer_top_arrow && (top_arrow_bleed > bottom_arrow_bleed)) || 390 (!prefer_top_arrow && (top_arrow_bleed >= bottom_arrow_bleed))) { 391 frame_style_none = CENTER_OVER_RECT; 392 frame_style_left = ANCHOR_BOTTOM_LEFT; 393 frame_style_middle = ANCHOR_BOTTOM_MIDDLE; 394 frame_style_right = ANCHOR_BOTTOM_RIGHT; 395 } 396 397 if (!HasArrow(preferred_style)) 398 return frame_style_none; 399 400 if (IsArrowMiddle(preferred_style)) 401 return frame_style_middle; 402 403 // Choose whether to show the bubble left or right of the specified location. 404 const bool prefer_left_arrow = IsArrowLeft(preferred_style); 405 // The bleed measures the amount of bubble that would be shown offscreen. 406 const int left_arrow_bleed = 407 std::max(width + arrow_x - kArrowX - screen_width, 0); 408 const int right_arrow_bleed = std::max(width - arrow_x - kArrowX, 0); 409 410 // Use the preferred location if it doesn't bleed more than the opposite side. 411 return ((prefer_left_arrow && (left_arrow_bleed <= right_arrow_bleed)) || 412 (!prefer_left_arrow && (left_arrow_bleed < right_arrow_bleed))) ? 413 frame_style_left : frame_style_right; 414 } 415 416 bool BubbleGtk::UpdateFrameStyle(bool force_move_and_reshape) { 417 if (!toplevel_window_ || !anchor_widget_) 418 return false; 419 420 gint toplevel_x = 0, toplevel_y = 0; 421 gdk_window_get_position(gtk_widget_get_window(toplevel_window_), 422 &toplevel_x, &toplevel_y); 423 int offset_x, offset_y; 424 gtk_widget_translate_coordinates(anchor_widget_, toplevel_window_, 425 rect_.x(), rect_.y(), &offset_x, &offset_y); 426 427 FrameStyle old_frame_style = actual_frame_style_; 428 GtkAllocation allocation; 429 gtk_widget_get_allocation(window_, &allocation); 430 actual_frame_style_ = GetAllowedFrameStyle( 431 requested_frame_style_, 432 toplevel_x + offset_x + (rect_.width() / 2), // arrow_x 433 toplevel_y + offset_y, 434 allocation.width, 435 allocation.height); 436 437 if (force_move_and_reshape || actual_frame_style_ != old_frame_style) { 438 UpdateWindowShape(); 439 MoveWindow(); 440 // We need to redraw the entire window to repaint its border. 441 gtk_widget_queue_draw(window_); 442 return true; 443 } 444 return false; 445 } 446 447 void BubbleGtk::UpdateWindowShape() { 448 if (mask_region_) { 449 gdk_region_destroy(mask_region_); 450 mask_region_ = NULL; 451 } 452 GtkAllocation allocation; 453 gtk_widget_get_allocation(window_, &allocation); 454 std::vector<GdkPoint> points = MakeFramePolygonPoints( 455 actual_frame_style_, allocation.width, allocation.height, 456 FRAME_MASK); 457 mask_region_ = gdk_region_polygon(&points[0], 458 points.size(), 459 GDK_EVEN_ODD_RULE); 460 461 GdkWindow* gdk_window = gtk_widget_get_window(window_); 462 gdk_window_shape_combine_region(gdk_window, NULL, 0, 0); 463 gdk_window_shape_combine_region(gdk_window, mask_region_, 0, 0); 464 } 465 466 void BubbleGtk::MoveWindow() { 467 if (!toplevel_window_ || !anchor_widget_) 468 return; 469 470 gint toplevel_x = 0, toplevel_y = 0; 471 gdk_window_get_position(gtk_widget_get_window(toplevel_window_), 472 &toplevel_x, &toplevel_y); 473 474 int offset_x, offset_y; 475 gtk_widget_translate_coordinates(anchor_widget_, toplevel_window_, 476 rect_.x(), rect_.y(), &offset_x, &offset_y); 477 478 gint screen_x = 0; 479 if (IsFixed(actual_frame_style_)) { 480 GtkAllocation toplevel_allocation; 481 gtk_widget_get_allocation(toplevel_window_, &toplevel_allocation); 482 483 GtkAllocation bubble_allocation; 484 gtk_widget_get_allocation(window_, &bubble_allocation); 485 486 int x_offset = actual_frame_style_ == FIXED_TOP_LEFT ? 487 kFixedPositionPaddingEnd : 488 toplevel_allocation.width - bubble_allocation.width - 489 kFixedPositionPaddingEnd; 490 screen_x = toplevel_x + x_offset; 491 } else if (!HasArrow(actual_frame_style_) || 492 IsArrowMiddle(actual_frame_style_)) { 493 GtkAllocation allocation; 494 gtk_widget_get_allocation(window_, &allocation); 495 screen_x = 496 toplevel_x + offset_x + (rect_.width() / 2) - allocation.width / 2; 497 } else if (IsArrowLeft(actual_frame_style_)) { 498 screen_x = toplevel_x + offset_x + (rect_.width() / 2) - kArrowX; 499 } else if (IsArrowRight(actual_frame_style_)) { 500 GtkAllocation allocation; 501 gtk_widget_get_allocation(window_, &allocation); 502 screen_x = toplevel_x + offset_x + (rect_.width() / 2) - 503 allocation.width + kArrowX; 504 } else { 505 NOTREACHED(); 506 } 507 508 gint screen_y = toplevel_y + offset_y + rect_.height(); 509 if (IsFixed(actual_frame_style_)) { 510 screen_y = toplevel_y + kFixedPositionPaddingTop; 511 } else if (IsArrowTop(actual_frame_style_) || 512 actual_frame_style_ == FLOAT_BELOW_RECT) { 513 screen_y += kArrowToContentPadding; 514 } else { 515 GtkAllocation allocation; 516 gtk_widget_get_allocation(window_, &allocation); 517 screen_y -= allocation.height + kArrowToContentPadding; 518 } 519 520 gtk_window_move(GTK_WINDOW(window_), screen_x, screen_y); 521 } 522 523 void BubbleGtk::StackWindow() { 524 // Stack our window directly above the toplevel window. 525 if (toplevel_window_) 526 ui::StackPopupWindow(window_, toplevel_window_); 527 } 528 529 void BubbleGtk::Observe(int type, 530 const content::NotificationSource& source, 531 const content::NotificationDetails& details) { 532 DCHECK_EQ(type, chrome::NOTIFICATION_BROWSER_THEME_CHANGED); 533 if (theme_service_->UsingNativeTheme() && match_system_theme_) { 534 gtk_widget_modify_bg(window_, GTK_STATE_NORMAL, NULL); 535 } else { 536 // Set the background color, so we don't need to paint it manually. 537 gtk_widget_modify_bg(window_, GTK_STATE_NORMAL, &kBackgroundColor); 538 } 539 } 540 541 void BubbleGtk::StopGrabbingInput() { 542 UngrabPointerAndKeyboard(); 543 if (!grab_input_) 544 return; 545 grab_input_ = false; 546 gtk_grab_remove(window_); 547 } 548 549 GtkWindow* BubbleGtk::GetNativeWindow() { 550 return GTK_WINDOW(window_); 551 } 552 553 void BubbleGtk::Close() { 554 // We don't need to ungrab the pointer or keyboard here; the X server will 555 // automatically do that when we destroy our window. 556 DCHECK(window_); 557 gtk_widget_destroy(window_); 558 // |this| has been deleted, see OnDestroy. 559 } 560 561 void BubbleGtk::SetPositionRelativeToAnchor(const gfx::Rect* rect) { 562 rect_ = rect ? *rect : gtk_util::WidgetBounds(anchor_widget_); 563 if (!UpdateFrameStyle(false)) 564 MoveWindow(); 565 } 566 567 void BubbleGtk::GrabPointerAndKeyboard() { 568 GdkWindow* gdk_window = gtk_widget_get_window(window_); 569 570 // Install X pointer and keyboard grabs to make sure that we have the focus 571 // and get all mouse and keyboard events until we're closed. As a hack, grab 572 // the pointer even if |grab_input_| is false to prevent a weird error 573 // rendering the bubble's frame. See 574 // https://code.google.com/p/chromium/issues/detail?id=130820. 575 GdkGrabStatus pointer_grab_status = 576 gdk_pointer_grab(gdk_window, 577 TRUE, // owner_events 578 GDK_BUTTON_PRESS_MASK, // event_mask 579 NULL, // confine_to 580 NULL, // cursor 581 GDK_CURRENT_TIME); 582 if (pointer_grab_status != GDK_GRAB_SUCCESS) { 583 // This will fail if someone else already has the pointer grabbed, but 584 // there's not really anything we can do about that. 585 DLOG(ERROR) << "Unable to grab pointer (status=" 586 << pointer_grab_status << ")"; 587 } 588 589 // Only grab the keyboard input if |grab_input_| is true. 590 if (grab_input_) { 591 GdkGrabStatus keyboard_grab_status = 592 gdk_keyboard_grab(gdk_window, 593 FALSE, // owner_events 594 GDK_CURRENT_TIME); 595 if (keyboard_grab_status != GDK_GRAB_SUCCESS) { 596 DLOG(ERROR) << "Unable to grab keyboard (status=" 597 << keyboard_grab_status << ")"; 598 } 599 } 600 } 601 602 void BubbleGtk::UngrabPointerAndKeyboard() { 603 gdk_pointer_ungrab(GDK_CURRENT_TIME); 604 if (grab_input_) 605 gdk_keyboard_ungrab(GDK_CURRENT_TIME); 606 } 607 608 gboolean BubbleGtk::OnGtkAccelerator(GtkAccelGroup* group, 609 GObject* acceleratable, 610 guint keyval, 611 GdkModifierType modifier) { 612 GdkEventKey msg; 613 GdkKeymapKey* keys; 614 gint n_keys; 615 616 switch (keyval) { 617 case GDK_Escape: 618 // Close on Esc and trap the accelerator 619 closed_by_escape_ = true; 620 Close(); 621 return TRUE; 622 case GDK_w: 623 // Close on C-w and forward the accelerator 624 if (modifier & GDK_CONTROL_MASK) { 625 gdk_keymap_get_entries_for_keyval(NULL, 626 keyval, 627 &keys, 628 &n_keys); 629 if (n_keys) { 630 // Forward the accelerator to root window the bubble is anchored 631 // to for further processing 632 msg.type = GDK_KEY_PRESS; 633 msg.window = gtk_widget_get_window(toplevel_window_); 634 msg.send_event = TRUE; 635 msg.time = GDK_CURRENT_TIME; 636 msg.state = modifier | GDK_MOD2_MASK; 637 msg.keyval = keyval; 638 // length and string are deprecated and thus zeroed out 639 msg.length = 0; 640 msg.string = NULL; 641 msg.hardware_keycode = keys[0].keycode; 642 msg.group = keys[0].group; 643 msg.is_modifier = 0; 644 645 g_free(keys); 646 647 gtk_main_do_event(reinterpret_cast<GdkEvent*>(&msg)); 648 } else { 649 // This means that there isn't a h/w code for the keyval in the 650 // current keymap, which is weird but possible if the keymap just 651 // changed. This isn't a critical error, but might be indicative 652 // of something off if it happens regularly. 653 DLOG(WARNING) << "Found no keys for value " << keyval; 654 } 655 Close(); 656 } 657 break; 658 default: 659 return FALSE; 660 } 661 662 return TRUE; 663 } 664 665 gboolean BubbleGtk::OnExpose(GtkWidget* widget, GdkEventExpose* expose) { 666 // TODO(erg): This whole method will need to be rewritten in cairo. 667 GdkDrawable* drawable = GDK_DRAWABLE(gtk_widget_get_window(window_)); 668 GdkGC* gc = gdk_gc_new(drawable); 669 gdk_gc_set_rgb_fg_color(gc, &kFrameColor); 670 671 // Stroke the frame border. 672 GtkAllocation allocation; 673 gtk_widget_get_allocation(window_, &allocation); 674 std::vector<GdkPoint> points = MakeFramePolygonPoints( 675 actual_frame_style_, allocation.width, allocation.height, 676 FRAME_STROKE); 677 gdk_draw_polygon(drawable, gc, FALSE, &points[0], points.size()); 678 679 // If |grab_input_| is false, pointer input has been grabbed as a hack in 680 // |GrabPointerAndKeyboard()| to ensure that the polygon frame is drawn 681 // correctly. Since the intention is not actually to grab the pointer, release 682 // it now that the frame is drawn to prevent clicks from being missed. See 683 // https://code.google.com/p/chromium/issues/detail?id=130820. 684 if (!grab_input_) 685 gdk_pointer_ungrab(GDK_CURRENT_TIME); 686 687 g_object_unref(gc); 688 return FALSE; // Propagate so our children paint, etc. 689 } 690 691 // When our size is initially allocated or changed, we need to recompute 692 // and apply our shape mask region. 693 void BubbleGtk::OnSizeAllocate(GtkWidget* widget, 694 GtkAllocation* allocation) { 695 if (!UpdateFrameStyle(false)) { 696 UpdateWindowShape(); 697 MoveWindow(); 698 } 699 } 700 701 gboolean BubbleGtk::OnButtonPress(GtkWidget* widget, 702 GdkEventButton* event) { 703 // If we got a click in our own window, that's okay (we need to additionally 704 // check that it falls within our bounds, since we've grabbed the pointer and 705 // some events that actually occurred in other windows will be reported with 706 // respect to our window). 707 GdkWindow* gdk_window = gtk_widget_get_window(window_); 708 if (event->window == gdk_window && 709 (mask_region_ && gdk_region_point_in(mask_region_, event->x, event->y))) { 710 return FALSE; // Propagate. 711 } 712 713 // Our content widget got a click. 714 if (event->window != gdk_window && 715 gdk_window_get_toplevel(event->window) == gdk_window) { 716 return FALSE; 717 } 718 719 if (grab_input_) { 720 // Otherwise we had a click outside of our window, close ourself. 721 Close(); 722 return TRUE; 723 } 724 725 return FALSE; 726 } 727 728 gboolean BubbleGtk::OnDestroy(GtkWidget* widget) { 729 // We are self deleting, we have a destroy signal setup to catch when we 730 // destroy the widget manually, or the window was closed via X. This will 731 // delete the BubbleGtk object. 732 delete this; 733 return FALSE; // Propagate. 734 } 735 736 void BubbleGtk::OnHide(GtkWidget* widget) { 737 gtk_widget_destroy(widget); 738 } 739 740 gboolean BubbleGtk::OnGrabBroken(GtkWidget* widget, 741 GdkEventGrabBroken* grab_broken) { 742 // |grab_input_| may have been changed to false. 743 if (!grab_input_) 744 return FALSE; 745 746 // |grab_window| can be NULL. 747 if (!grab_broken->grab_window) 748 return FALSE; 749 750 gpointer user_data; 751 gdk_window_get_user_data(grab_broken->grab_window, &user_data); 752 753 if (GTK_IS_WIDGET(user_data)) { 754 signals_.Connect(GTK_WIDGET(user_data), "hide", 755 G_CALLBACK(OnForeshadowWidgetHideThunk), this); 756 } 757 758 return FALSE; 759 } 760 761 void BubbleGtk::OnForeshadowWidgetHide(GtkWidget* widget) { 762 if (grab_input_) 763 GrabPointerAndKeyboard(); 764 765 signals_.DisconnectAll(widget); 766 } 767 768 gboolean BubbleGtk::OnToplevelConfigure(GtkWidget* widget, 769 GdkEventConfigure* event) { 770 if (!UpdateFrameStyle(false)) 771 MoveWindow(); 772 StackWindow(); 773 return FALSE; 774 } 775 776 gboolean BubbleGtk::OnToplevelUnmap(GtkWidget* widget, GdkEvent* event) { 777 Close(); 778 return FALSE; 779 } 780 781 void BubbleGtk::OnAnchorAllocate(GtkWidget* widget, 782 GtkAllocation* allocation) { 783 if (!UpdateFrameStyle(false)) 784 MoveWindow(); 785 } 786 787 void BubbleGtk::OnAnchorDestroy(GtkWidget* widget) { 788 anchor_widget_ = NULL; 789 790 // Ctrl-W will first destroy the anchor, then call |this| back via 791 // |accel_group_|. So that the callback |this| registered via |accel_group_| 792 // doesn't run after |this| is destroyed, we delay this close (which, unlike 793 // the accelerator callback, will be cancelled if |this| is destroyed). 794 // http://crbug.com/286621 795 base::MessageLoop::current()->PostTask( 796 FROM_HERE, 797 base::Bind(&BubbleGtk::Close, weak_ptr_factory_.GetWeakPtr())); 798 } 799