Home | History | Annotate | Download | only in tabs
      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/tabs/tab_gtk.h"
      6 
      7 #include <gdk/gdkkeysyms.h>
      8 
      9 #include "base/bind.h"
     10 #include "base/debug/trace_event.h"
     11 #include "base/memory/singleton.h"
     12 #include "base/strings/utf_string_conversions.h"
     13 #include "chrome/app/chrome_command_ids.h"
     14 #include "chrome/browser/ui/gtk/accelerators_gtk.h"
     15 #include "chrome/browser/ui/gtk/gtk_input_event_box.h"
     16 #include "chrome/browser/ui/gtk/menu_gtk.h"
     17 #include "chrome/browser/ui/gtk/tabs/tab_strip_menu_controller.h"
     18 #include "chrome/browser/ui/tabs/tab_menu_model.h"
     19 #include "chrome/browser/ui/tabs/tab_resources.h"
     20 #include "grit/generated_resources.h"
     21 #include "grit/theme_resources.h"
     22 #include "ui/base/dragdrop/gtk_dnd_util.h"
     23 #include "ui/base/gtk/scoped_region.h"
     24 #include "ui/gfx/path.h"
     25 
     26 using content::WebContents;
     27 
     28 namespace {
     29 
     30 // Returns the width of the title for the current font, in pixels.
     31 int GetTitleWidth(gfx::Font* font, string16 title) {
     32   DCHECK(font);
     33   if (title.empty())
     34     return 0;
     35 
     36   return font->GetStringWidth(title);
     37 }
     38 
     39 }  // namespace
     40 
     41 class TabGtk::TabGtkObserverHelper {
     42  public:
     43   explicit TabGtkObserverHelper(TabGtk* tab)
     44       : tab_(tab) {
     45     base::MessageLoopForUI::current()->AddObserver(tab_);
     46   }
     47 
     48   ~TabGtkObserverHelper() {
     49     base::MessageLoopForUI::current()->RemoveObserver(tab_);
     50   }
     51 
     52  private:
     53   TabGtk* tab_;
     54 
     55   DISALLOW_COPY_AND_ASSIGN(TabGtkObserverHelper);
     56 };
     57 
     58 ///////////////////////////////////////////////////////////////////////////////
     59 // TabGtk, public:
     60 
     61 TabGtk::TabGtk(TabDelegate* delegate)
     62     : TabRendererGtk(delegate->GetThemeProvider()),
     63       delegate_(delegate),
     64       closing_(false),
     65       dragging_(false),
     66       last_mouse_down_(NULL),
     67       drag_widget_(NULL),
     68       title_width_(0),
     69       destroy_factory_(this),
     70       drag_end_factory_(this) {
     71   event_box_ = gtk_input_event_box_new();
     72   g_signal_connect(event_box_, "button-press-event",
     73                    G_CALLBACK(OnButtonPressEventThunk), this);
     74   g_signal_connect(event_box_, "button-release-event",
     75                    G_CALLBACK(OnButtonReleaseEventThunk), this);
     76   g_signal_connect(event_box_, "enter-notify-event",
     77                    G_CALLBACK(OnEnterNotifyEventThunk), this);
     78   g_signal_connect(event_box_, "leave-notify-event",
     79                    G_CALLBACK(OnLeaveNotifyEventThunk), this);
     80   gtk_widget_add_events(event_box_,
     81         GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
     82         GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK);
     83   gtk_container_add(GTK_CONTAINER(event_box_), TabRendererGtk::widget());
     84   gtk_widget_show_all(event_box_);
     85 }
     86 
     87 TabGtk::~TabGtk() {
     88   if (drag_widget_) {
     89     // Shadow the drag grab so the grab terminates. We could do this using any
     90     // widget, |drag_widget_| is just convenient.
     91     gtk_grab_add(drag_widget_);
     92     gtk_grab_remove(drag_widget_);
     93     DestroyDragWidget();
     94   }
     95 
     96   if (menu_controller_.get()) {
     97     // The menu is showing. Close the menu.
     98     menu_controller_->Cancel();
     99 
    100     // Invoke this so that we hide the highlight.
    101     ContextMenuClosed();
    102   }
    103 }
    104 
    105 void TabGtk::Raise() const {
    106   UNSHIPPED_TRACE_EVENT0("ui::gtk", "TabGtk::Raise");
    107 
    108   GdkWindow* window = gtk_input_event_box_get_window(
    109       GTK_INPUT_EVENT_BOX(event_box_));
    110   gdk_window_raise(window);
    111   TabRendererGtk::Raise();
    112 }
    113 
    114 gboolean TabGtk::OnButtonPressEvent(GtkWidget* widget, GdkEventButton* event) {
    115   // Every button press ensures either a button-release-event or a drag-fail
    116   // signal for |widget|.
    117   if (event->button == 1 && event->type == GDK_BUTTON_PRESS) {
    118     // Store whether or not we were selected just now... we only want to be
    119     // able to drag foreground tabs, so we don't start dragging the tab if
    120     // it was in the background.
    121     if (!IsActive()) {
    122       if (event->state & GDK_CONTROL_MASK)
    123         delegate_->ToggleTabSelection(this);
    124       else if (event->state & GDK_SHIFT_MASK)
    125         delegate_->ExtendTabSelection(this);
    126       else
    127         delegate_->ActivateTab(this);
    128     }
    129     // Hook into the message loop to handle dragging.
    130     observer_.reset(new TabGtkObserverHelper(this));
    131 
    132     // Store the button press event, used to initiate a drag.
    133     last_mouse_down_ = gdk_event_copy(reinterpret_cast<GdkEvent*>(event));
    134   } else if (event->button == 3) {
    135     // Only show the context menu if the left mouse button isn't down (i.e.,
    136     // the user might want to drag instead).
    137     if (!last_mouse_down_) {
    138       menu_controller_.reset(delegate()->GetTabStripMenuControllerForTab(this));
    139       menu_controller_->RunMenu(gfx::Point(event->x_root, event->y_root),
    140                                 event->time);
    141     }
    142   }
    143 
    144   return TRUE;
    145 }
    146 
    147 gboolean TabGtk::OnButtonReleaseEvent(GtkWidget* widget,
    148                                       GdkEventButton* event) {
    149   if (event->button == 1) {
    150     if (IsActive() && !(event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK))) {
    151       delegate_->ActivateTab(this);
    152     }
    153     observer_.reset();
    154 
    155     if (last_mouse_down_) {
    156       gdk_event_free(last_mouse_down_);
    157       last_mouse_down_ = NULL;
    158     }
    159   }
    160 
    161   GtkAllocation allocation;
    162   gtk_widget_get_allocation(widget, &allocation);
    163 
    164   // Middle mouse up means close the tab, but only if the mouse is over it
    165   // (like a button).
    166   if (event->button == 2 &&
    167       event->x >= 0 && event->y >= 0 &&
    168       event->x < allocation.width &&
    169       event->y < allocation.height) {
    170     // If the user is currently holding the left mouse button down but hasn't
    171     // moved the mouse yet, a drag hasn't started yet.  In that case, clean up
    172     // some state before closing the tab to avoid a crash.  Once the drag has
    173     // started, we don't get the middle mouse click here.
    174     if (last_mouse_down_) {
    175       DCHECK(!drag_widget_);
    176       observer_.reset();
    177       gdk_event_free(last_mouse_down_);
    178       last_mouse_down_ = NULL;
    179     }
    180     delegate_->CloseTab(this);
    181   }
    182 
    183   return TRUE;
    184 }
    185 
    186 gboolean TabGtk::OnDragFailed(GtkWidget* widget, GdkDragContext* context,
    187                               GtkDragResult result) {
    188   bool canceled = (result == GTK_DRAG_RESULT_USER_CANCELLED);
    189   EndDrag(canceled);
    190   return TRUE;
    191 }
    192 
    193 gboolean TabGtk::OnDragButtonReleased(GtkWidget* widget,
    194                                       GdkEventButton* button) {
    195   // We always get this event when gtk is releasing the grab and ending the
    196   // drag.  However, if the user ended the drag with space or enter, we don't
    197   // get a follow up event to tell us the drag has finished (either a
    198   // drag-failed or a drag-end).  So we post a task to manually end the drag.
    199   // If GTK+ does send the drag-failed or drag-end event, we cancel the task.
    200   base::MessageLoop::current()->PostTask(
    201       FROM_HERE,
    202       base::Bind(&TabGtk::EndDrag, drag_end_factory_.GetWeakPtr(), false));
    203   return TRUE;
    204 }
    205 
    206 void TabGtk::OnDragBegin(GtkWidget* widget, GdkDragContext* context) {
    207   GdkPixbuf* pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, TRUE, 8, 1, 1);
    208   gdk_pixbuf_fill(pixbuf, 0);
    209   gtk_drag_set_icon_pixbuf(context, pixbuf, 0, 0);
    210   g_object_unref(pixbuf);
    211 }
    212 
    213 ///////////////////////////////////////////////////////////////////////////////
    214 // TabGtk, MessageLoop::Observer implementation:
    215 
    216 void TabGtk::WillProcessEvent(GdkEvent* event) {
    217   // Nothing to do.
    218 }
    219 
    220 void TabGtk::DidProcessEvent(GdkEvent* event) {
    221   if (!(event->type == GDK_MOTION_NOTIFY || event->type == GDK_LEAVE_NOTIFY ||
    222         event->type == GDK_ENTER_NOTIFY)) {
    223     return;
    224   }
    225 
    226   if (drag_widget_) {
    227     delegate_->ContinueDrag(NULL);
    228     return;
    229   }
    230 
    231   gint old_x = static_cast<gint>(last_mouse_down_->button.x_root);
    232   gint old_y = static_cast<gint>(last_mouse_down_->button.y_root);
    233   gdouble new_x;
    234   gdouble new_y;
    235   gdk_event_get_root_coords(event, &new_x, &new_y);
    236 
    237   if (gtk_drag_check_threshold(widget(), old_x, old_y,
    238       static_cast<gint>(new_x), static_cast<gint>(new_y))) {
    239     StartDragging(gfx::Point(
    240         static_cast<int>(last_mouse_down_->button.x),
    241         static_cast<int>(last_mouse_down_->button.y)));
    242   }
    243 }
    244 
    245 ///////////////////////////////////////////////////////////////////////////////
    246 // TabGtk, TabRendererGtk overrides:
    247 
    248 bool TabGtk::IsActive() const {
    249   return delegate_->IsTabActive(this);
    250 }
    251 
    252 bool TabGtk::IsSelected() const {
    253   return delegate_->IsTabSelected(this);
    254 }
    255 
    256 bool TabGtk::IsVisible() const {
    257   return gtk_widget_get_visible(event_box_);
    258 }
    259 
    260 void TabGtk::SetVisible(bool visible) const {
    261   gtk_widget_set_visible(event_box_, visible);
    262 }
    263 
    264 void TabGtk::CloseButtonClicked() {
    265   delegate_->CloseTab(this);
    266 }
    267 
    268 void TabGtk::UpdateData(WebContents* contents, bool app, bool loading_only) {
    269   TabRendererGtk::UpdateData(contents, app, loading_only);
    270   // Cache the title width so we don't recalculate it every time the tab is
    271   // resized.
    272   title_width_ = GetTitleWidth(title_font(), GetTitle());
    273   UpdateTooltipState();
    274 }
    275 
    276 void TabGtk::SetBounds(const gfx::Rect& bounds) {
    277   TabRendererGtk::SetBounds(bounds);
    278 
    279   if (gtk_input_event_box_get_window(GTK_INPUT_EVENT_BOX(event_box_))) {
    280     gfx::Path mask;
    281     TabResources::GetHitTestMask(bounds.width(), bounds.height(), false, &mask);
    282     ui::ScopedRegion region(mask.CreateNativeRegion());
    283     gdk_window_input_shape_combine_region(
    284         gtk_input_event_box_get_window(GTK_INPUT_EVENT_BOX(event_box_)),
    285         region.Get(),
    286         0, 0);
    287   }
    288 
    289   UpdateTooltipState();
    290 }
    291 
    292 ///////////////////////////////////////////////////////////////////////////////
    293 // TabGtk, private:
    294 
    295 void TabGtk::ContextMenuClosed() {
    296   delegate()->StopAllHighlighting();
    297   menu_controller_.reset();
    298 }
    299 
    300 void TabGtk::UpdateTooltipState() {
    301   // Only show the tooltip if the title is truncated.
    302   if (title_width_ > title_bounds().width()) {
    303     gtk_widget_set_tooltip_text(widget(), UTF16ToUTF8(GetTitle()).c_str());
    304   } else {
    305     gtk_widget_set_has_tooltip(widget(), FALSE);
    306   }
    307 }
    308 
    309 void TabGtk::CreateDragWidget() {
    310   DCHECK(!drag_widget_);
    311   drag_widget_ = gtk_invisible_new();
    312   g_signal_connect(drag_widget_, "drag-failed",
    313                    G_CALLBACK(OnDragFailedThunk), this);
    314   g_signal_connect(drag_widget_, "button-release-event",
    315                    G_CALLBACK(OnDragButtonReleasedThunk), this);
    316   g_signal_connect_after(drag_widget_, "drag-begin",
    317                          G_CALLBACK(OnDragBeginThunk), this);
    318 }
    319 
    320 void TabGtk::DestroyDragWidget() {
    321   if (drag_widget_) {
    322     gtk_widget_destroy(drag_widget_);
    323     drag_widget_ = NULL;
    324   }
    325 }
    326 
    327 void TabGtk::StartDragging(gfx::Point drag_offset) {
    328   // If the drag is processed after the selection change it's possible
    329   // that the tab has been deselected, in which case we don't want to drag.
    330   if (!IsSelected())
    331     return;
    332 
    333   CreateDragWidget();
    334 
    335   GtkTargetList* list = ui::GetTargetListFromCodeMask(ui::CHROME_TAB);
    336   gtk_drag_begin(drag_widget_, list, GDK_ACTION_MOVE,
    337                  1,  // Drags are always initiated by the left button.
    338                  last_mouse_down_);
    339   // gtk_drag_begin adds a reference to list, so unref it here.
    340   gtk_target_list_unref(list);
    341   delegate_->MaybeStartDrag(this, drag_offset);
    342 }
    343 
    344 void TabGtk::EndDrag(bool canceled) {
    345   // Make sure we only run EndDrag once by canceling any tasks that want
    346   // to call EndDrag.
    347   drag_end_factory_.InvalidateWeakPtrs();
    348 
    349   // We must let gtk clean up after we handle the drag operation, otherwise
    350   // there will be outstanding references to the drag widget when we try to
    351   // destroy it.
    352   base::MessageLoop::current()->PostTask(
    353       FROM_HERE,
    354       base::Bind(&TabGtk::DestroyDragWidget, destroy_factory_.GetWeakPtr()));
    355 
    356   if (last_mouse_down_) {
    357     gdk_event_free(last_mouse_down_);
    358     last_mouse_down_ = NULL;
    359   }
    360 
    361   // Notify the drag helper that we're done with any potential drag operations.
    362   // Clean up the drag helper, which is re-created on the next mouse press.
    363   delegate_->EndDrag(canceled);
    364 
    365   observer_.reset();
    366 }
    367