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