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.
5 #include "chrome/browser/ui/gtk/tabs/tab_gtk.h"
7 #include <gdk/gdkkeysyms.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"
26 using content::WebContents
;
30 // Returns the width of the title for the current font, in pixels.
31 int GetTitleWidth(gfx::Font
* font
, base::string16 title
) {
36 return font
->GetStringWidth(title
);
41 class TabGtk::TabGtkObserverHelper
{
43 explicit TabGtkObserverHelper(TabGtk
* tab
)
45 base::MessageLoopForUI::current()->AddObserver(tab_
);
48 ~TabGtkObserverHelper() {
49 base::MessageLoopForUI::current()->RemoveObserver(tab_
);
55 DISALLOW_COPY_AND_ASSIGN(TabGtkObserverHelper
);
58 ///////////////////////////////////////////////////////////////////////////////
61 TabGtk::TabGtk(TabDelegate
* delegate
)
62 : TabRendererGtk(delegate
->GetThemeProvider()),
66 last_mouse_down_(NULL
),
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_
);
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_
);
96 if (menu_controller_
.get()) {
97 // The menu is showing. Close the menu.
98 menu_controller_
->Cancel();
100 // Invoke this so that we hide the highlight.
105 void TabGtk::Raise() const {
106 UNSHIPPED_TRACE_EVENT0("ui::gtk", "TabGtk::Raise");
108 GdkWindow
* window
= gtk_input_event_box_get_window(
109 GTK_INPUT_EVENT_BOX(event_box_
));
110 gdk_window_raise(window
);
111 TabRendererGtk::Raise();
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.
122 if (event
->state
& GDK_CONTROL_MASK
)
123 delegate_
->ToggleTabSelection(this);
124 else if (event
->state
& GDK_SHIFT_MASK
)
125 delegate_
->ExtendTabSelection(this);
127 delegate_
->ActivateTab(this);
129 // Hook into the message loop to handle dragging.
130 observer_
.reset(new TabGtkObserverHelper(this));
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
),
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);
155 if (last_mouse_down_
) {
156 gdk_event_free(last_mouse_down_
);
157 last_mouse_down_
= NULL
;
161 GtkAllocation allocation
;
162 gtk_widget_get_allocation(widget
, &allocation
);
164 // Middle mouse up means close the tab, but only if the mouse is over it
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_
);
177 gdk_event_free(last_mouse_down_
);
178 last_mouse_down_
= NULL
;
180 delegate_
->CloseTab(this);
186 gboolean
TabGtk::OnDragFailed(GtkWidget
* widget
, GdkDragContext
* context
,
187 GtkDragResult result
) {
188 bool canceled
= (result
== GTK_DRAG_RESULT_USER_CANCELLED
);
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(
202 base::Bind(&TabGtk::EndDrag
, drag_end_factory_
.GetWeakPtr(), false));
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
);
213 ///////////////////////////////////////////////////////////////////////////////
214 // TabGtk, MessageLoop::Observer implementation:
216 void TabGtk::WillProcessEvent(GdkEvent
* event
) {
220 void TabGtk::DidProcessEvent(GdkEvent
* event
) {
221 if (!(event
->type
== GDK_MOTION_NOTIFY
|| event
->type
== GDK_LEAVE_NOTIFY
||
222 event
->type
== GDK_ENTER_NOTIFY
)) {
227 delegate_
->ContinueDrag(NULL
);
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
);
235 gdk_event_get_root_coords(event
, &new_x
, &new_y
);
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
)));
245 ///////////////////////////////////////////////////////////////////////////////
246 // TabGtk, TabRendererGtk overrides:
248 bool TabGtk::IsActive() const {
249 return delegate_
->IsTabActive(this);
252 bool TabGtk::IsSelected() const {
253 return delegate_
->IsTabSelected(this);
256 bool TabGtk::IsVisible() const {
257 return gtk_widget_get_visible(event_box_
);
260 void TabGtk::SetVisible(bool visible
) const {
261 gtk_widget_set_visible(event_box_
, visible
);
264 void TabGtk::CloseButtonClicked() {
265 delegate_
->CloseTab(this);
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
272 title_width_
= GetTitleWidth(title_font(), GetTitle());
273 UpdateTooltipState();
276 void TabGtk::SetBounds(const gfx::Rect
& bounds
) {
277 TabRendererGtk::SetBounds(bounds
);
279 if (gtk_input_event_box_get_window(GTK_INPUT_EVENT_BOX(event_box_
))) {
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_
)),
289 UpdateTooltipState();
292 ///////////////////////////////////////////////////////////////////////////////
295 void TabGtk::ContextMenuClosed() {
296 delegate()->StopAllHighlighting();
297 menu_controller_
.reset();
300 void TabGtk::UpdateTooltipState() {
301 // Note: This method can be called several times per second for various
302 // reasons (e.g., navigation/loading state changes, tab media indicator
303 // updates, and so on). However, we must avoid calling the
304 // gtk_widget_set_XXX() methods if there is no actual change in state.
305 // Otherwise, GTK will continuously re-hide *all* tooltips throughout the
306 // browser in response! http://crbug.com/333002
308 // Only show the tooltip if the title is truncated.
309 if (title_width_
> title_bounds().width()) {
310 const std::string utf8_title
= base::UTF16ToUTF8(GetTitle());
311 if (gtk_widget_get_has_tooltip(widget())) {
312 gchar
* const current_tooltip
= gtk_widget_get_tooltip_text(widget());
313 if (current_tooltip
) {
314 const bool title_unchanged
= (utf8_title
== current_tooltip
);
315 g_free(current_tooltip
);
320 gtk_widget_set_tooltip_text(widget(), utf8_title
.c_str());
322 if (gtk_widget_get_has_tooltip(widget()))
323 gtk_widget_set_has_tooltip(widget(), FALSE
);
327 void TabGtk::CreateDragWidget() {
328 DCHECK(!drag_widget_
);
329 drag_widget_
= gtk_invisible_new();
330 g_signal_connect(drag_widget_
, "drag-failed",
331 G_CALLBACK(OnDragFailedThunk
), this);
332 g_signal_connect(drag_widget_
, "button-release-event",
333 G_CALLBACK(OnDragButtonReleasedThunk
), this);
334 g_signal_connect_after(drag_widget_
, "drag-begin",
335 G_CALLBACK(OnDragBeginThunk
), this);
338 void TabGtk::DestroyDragWidget() {
340 gtk_widget_destroy(drag_widget_
);
345 void TabGtk::StartDragging(gfx::Point drag_offset
) {
346 // If the drag is processed after the selection change it's possible
347 // that the tab has been deselected, in which case we don't want to drag.
353 GtkTargetList
* list
= ui::GetTargetListFromCodeMask(ui::CHROME_TAB
);
354 gtk_drag_begin(drag_widget_
, list
, GDK_ACTION_MOVE
,
355 1, // Drags are always initiated by the left button.
357 // gtk_drag_begin adds a reference to list, so unref it here.
358 gtk_target_list_unref(list
);
359 delegate_
->MaybeStartDrag(this, drag_offset
);
362 void TabGtk::EndDrag(bool canceled
) {
363 // Make sure we only run EndDrag once by canceling any tasks that want
365 drag_end_factory_
.InvalidateWeakPtrs();
367 // We must let gtk clean up after we handle the drag operation, otherwise
368 // there will be outstanding references to the drag widget when we try to
370 base::MessageLoop::current()->PostTask(
372 base::Bind(&TabGtk::DestroyDragWidget
, destroy_factory_
.GetWeakPtr()));
374 if (last_mouse_down_
) {
375 gdk_event_free(last_mouse_down_
);
376 last_mouse_down_
= NULL
;
379 // Notify the drag helper that we're done with any potential drag operations.
380 // Clean up the drag helper, which is re-created on the next mouse press.
381 delegate_
->EndDrag(canceled
);