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