Home | History | Annotate | Download | only in gtk
      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