• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
GetTitleWidth(gfx::Font * font,string16 title)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:
ContextMenuController(TabGtk * tab)37   explicit ContextMenuController(TabGtk* tab)
38       : tab_(tab),
39         model_(this, tab->delegate()->IsTabPinned(tab)) {
40     menu_.reset(new MenuGtk(this, &model_));
41   }
42 
~ContextMenuController()43   virtual ~ContextMenuController() {}
44 
RunMenu(const gfx::Point & point,guint32 event_time)45   void RunMenu(const gfx::Point& point, guint32 event_time) {
46     menu_->PopupAsContext(point, event_time);
47   }
48 
Cancel()49   void Cancel() {
50     tab_ = NULL;
51     menu_->Cancel();
52   }
53 
54  private:
55   // Overridden from ui::SimpleMenuModel::Delegate:
IsCommandIdChecked(int command_id) const56   virtual bool IsCommandIdChecked(int command_id) const {
57     return false;
58   }
IsCommandIdEnabled(int command_id) const59   virtual bool IsCommandIdEnabled(int command_id) const {
60     return tab_ && tab_->delegate()->IsCommandEnabledForTab(
61         static_cast<TabStripModel::ContextMenuCommand>(command_id),
62         tab_);
63   }
GetAcceleratorForCommandId(int command_id,ui::Accelerator * accelerator)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 
ExecuteCommand(int command_id)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 
GetImageForCommandId(int command_id) const86   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:
TabGtkObserverHelper(TabGtk * tab)109   explicit TabGtkObserverHelper(TabGtk* tab)
110       : tab_(tab) {
111     MessageLoopForUI::current()->AddObserver(tab_);
112   }
113 
~TabGtkObserverHelper()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 
TabGtk(TabDelegate * delegate)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 
~TabGtk()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 
OnButtonPressEvent(GtkWidget * widget,GdkEventButton * event)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 
OnButtonReleaseEvent(GtkWidget * widget,GdkEventButton * event)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 
OnDragFailed(GtkWidget * widget,GdkDragContext * context,GtkDragResult result)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 
OnDragButtonReleased(GtkWidget * widget,GdkEventButton * button)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 
OnDragBegin(GtkWidget * widget,GdkDragContext * context)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 
WillProcessEvent(GdkEvent * event)264 void TabGtk::WillProcessEvent(GdkEvent* event) {
265   // Nothing to do.
266 }
267 
DidProcessEvent(GdkEvent * event)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 
IsSelected() const296 bool TabGtk::IsSelected() const {
297   return delegate_->IsTabSelected(this);
298 }
299 
IsVisible() const300 bool TabGtk::IsVisible() const {
301   return GTK_WIDGET_FLAGS(event_box_) & GTK_VISIBLE;
302 }
303 
SetVisible(bool visible) const304 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 
CloseButtonClicked()312 void TabGtk::CloseButtonClicked() {
313   delegate_->CloseTab(this);
314 }
315 
UpdateData(TabContents * contents,bool app,bool loading_only)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 
SetBounds(const gfx::Rect & bounds)324 void TabGtk::SetBounds(const gfx::Rect& bounds) {
325   TabRendererGtk::SetBounds(bounds);
326   UpdateTooltipState();
327 }
328 
329 ///////////////////////////////////////////////////////////////////////////////
330 // TabGtk, private:
331 
ContextMenuClosed()332 void TabGtk::ContextMenuClosed() {
333   delegate()->StopAllHighlighting();
334   menu_controller_.reset();
335 }
336 
UpdateTooltipState()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 
CreateDragWidget()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 
DestroyDragWidget()357 void TabGtk::DestroyDragWidget() {
358   if (drag_widget_) {
359     gtk_widget_destroy(drag_widget_);
360     drag_widget_ = NULL;
361   }
362 }
363 
StartDragging(gfx::Point drag_offset)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 
EndDrag(bool canceled)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