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