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
21namespace {
22
23// Returns the width of the title for the current font, in pixels.
24int 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
34class 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
107class 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
127TabGtk::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
154TabGtk::~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
172gboolean 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
202gboolean 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
235gboolean 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
242gboolean 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
254void 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
264void TabGtk::WillProcessEvent(GdkEvent* event) {
265  // Nothing to do.
266}
267
268void 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
296bool TabGtk::IsSelected() const {
297  return delegate_->IsTabSelected(this);
298}
299
300bool TabGtk::IsVisible() const {
301  return GTK_WIDGET_FLAGS(event_box_) & GTK_VISIBLE;
302}
303
304void 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
312void TabGtk::CloseButtonClicked() {
313  delegate_->CloseTab(this);
314}
315
316void 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
324void TabGtk::SetBounds(const gfx::Rect& bounds) {
325  TabRendererGtk::SetBounds(bounds);
326  UpdateTooltipState();
327}
328
329///////////////////////////////////////////////////////////////////////////////
330// TabGtk, private:
331
332void TabGtk::ContextMenuClosed() {
333  delegate()->StopAllHighlighting();
334  menu_controller_.reset();
335}
336
337void 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
346void 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
357void TabGtk::DestroyDragWidget() {
358  if (drag_widget_) {
359    gtk_widget_destroy(drag_widget_);
360    drag_widget_ = NULL;
361  }
362}
363
364void 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
375void 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