| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2010 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/gtk/tabs/tab_gtk.h" | |
| 6 | |
| 7 #include <gdk/gdkkeysyms.h> | |
| 8 | |
| 9 #include "app/gtk_dnd_util.h" | |
| 10 #include "app/menus/accelerator_gtk.h" | |
| 11 #include "base/singleton.h" | |
| 12 #include "base/utf_string_conversions.h" | |
| 13 #include "chrome/app/chrome_command_ids.h" | |
| 14 #include "chrome/browser/gtk/accelerators_gtk.h" | |
| 15 #include "chrome/browser/gtk/menu_gtk.h" | |
| 16 #include "chrome/browser/ui/tabs/tab_menu_model.h" | |
| 17 #include "gfx/path.h" | |
| 18 #include "grit/generated_resources.h" | |
| 19 #include "grit/theme_resources.h" | |
| 20 | |
| 21 namespace { | |
| 22 | |
| 23 // Returns the width of the title for the current font, in pixels. | |
| 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 menus::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() { | |
| 46 menu_->PopupAsContext(gtk_get_current_event_time()); | |
| 47 } | |
| 48 | |
| 49 void Cancel() { | |
| 50 tab_ = NULL; | |
| 51 menu_->Cancel(); | |
| 52 } | |
| 53 | |
| 54 private: | |
| 55 // Overridden from menus::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 menus::Accelerator* accelerator) { | |
| 67 int browser_command; | |
| 68 if (!TabStripModel::ContextMenuCommandToBrowserCommand(command_id, | |
| 69 &browser_command)) | |
| 70 return false; | |
| 71 const menus::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 | |
| 107 class 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 | |
| 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 | |
| 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 | |
| 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 ShowContextMenu(); | |
| 194 } | |
| 195 | |
| 196 return TRUE; | |
| 197 } | |
| 198 | |
| 199 gboolean TabGtk::OnButtonReleaseEvent(GtkWidget* widget, | |
| 200 GdkEventButton* event) { | |
| 201 if (event->button == 1) { | |
| 202 observer_.reset(); | |
| 203 | |
| 204 if (last_mouse_down_) { | |
| 205 gdk_event_free(last_mouse_down_); | |
| 206 last_mouse_down_ = NULL; | |
| 207 } | |
| 208 } | |
| 209 | |
| 210 // Middle mouse up means close the tab, but only if the mouse is over it | |
| 211 // (like a button). | |
| 212 if (event->button == 2 && | |
| 213 event->x >= 0 && event->y >= 0 && | |
| 214 event->x < widget->allocation.width && | |
| 215 event->y < widget->allocation.height) { | |
| 216 // If the user is currently holding the left mouse button down but hasn't | |
| 217 // moved the mouse yet, a drag hasn't started yet. In that case, clean up | |
| 218 // some state before closing the tab to avoid a crash. Once the drag has | |
| 219 // started, we don't get the middle mouse click here. | |
| 220 if (last_mouse_down_) { | |
| 221 DCHECK(!drag_widget_); | |
| 222 observer_.reset(); | |
| 223 gdk_event_free(last_mouse_down_); | |
| 224 last_mouse_down_ = NULL; | |
| 225 } | |
| 226 delegate_->CloseTab(this); | |
| 227 } | |
| 228 | |
| 229 return TRUE; | |
| 230 } | |
| 231 | |
| 232 gboolean TabGtk::OnDragFailed(GtkWidget* widget, GdkDragContext* context, | |
| 233 GtkDragResult result) { | |
| 234 bool canceled = (result == GTK_DRAG_RESULT_USER_CANCELLED); | |
| 235 EndDrag(canceled); | |
| 236 return TRUE; | |
| 237 } | |
| 238 | |
| 239 gboolean TabGtk::OnDragButtonReleased(GtkWidget* widget, | |
| 240 GdkEventButton* button) { | |
| 241 // We always get this event when gtk is releasing the grab and ending the | |
| 242 // drag. However, if the user ended the drag with space or enter, we don't | |
| 243 // get a follow up event to tell us the drag has finished (either a | |
| 244 // drag-failed or a drag-end). So we post a task to manually end the drag. | |
| 245 // If GTK+ does send the drag-failed or drag-end event, we cancel the task. | |
| 246 MessageLoop::current()->PostTask(FROM_HERE, | |
| 247 drag_end_factory_.NewRunnableMethod(&TabGtk::EndDrag, false)); | |
| 248 return TRUE; | |
| 249 } | |
| 250 | |
| 251 void TabGtk::OnDragBegin(GtkWidget* widget, GdkDragContext* context) { | |
| 252 GdkPixbuf* pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, TRUE, 8, 1, 1); | |
| 253 gtk_drag_set_icon_pixbuf(context, pixbuf, 0, 0); | |
| 254 g_object_unref(pixbuf); | |
| 255 } | |
| 256 | |
| 257 /////////////////////////////////////////////////////////////////////////////// | |
| 258 // TabGtk, MessageLoop::Observer implementation: | |
| 259 | |
| 260 void TabGtk::WillProcessEvent(GdkEvent* event) { | |
| 261 // Nothing to do. | |
| 262 } | |
| 263 | |
| 264 void TabGtk::DidProcessEvent(GdkEvent* event) { | |
| 265 if (!(event->type == GDK_MOTION_NOTIFY || event->type == GDK_LEAVE_NOTIFY || | |
| 266 event->type == GDK_ENTER_NOTIFY)) { | |
| 267 return; | |
| 268 } | |
| 269 | |
| 270 if (drag_widget_) { | |
| 271 delegate_->ContinueDrag(NULL); | |
| 272 return; | |
| 273 } | |
| 274 | |
| 275 gint old_x = static_cast<gint>(last_mouse_down_->button.x_root); | |
| 276 gint old_y = static_cast<gint>(last_mouse_down_->button.y_root); | |
| 277 gdouble new_x; | |
| 278 gdouble new_y; | |
| 279 gdk_event_get_root_coords(event, &new_x, &new_y); | |
| 280 | |
| 281 if (gtk_drag_check_threshold(widget(), old_x, old_y, | |
| 282 static_cast<gint>(new_x), static_cast<gint>(new_y))) { | |
| 283 StartDragging(gfx::Point( | |
| 284 static_cast<int>(last_mouse_down_->button.x), | |
| 285 static_cast<int>(last_mouse_down_->button.y))); | |
| 286 } | |
| 287 } | |
| 288 | |
| 289 /////////////////////////////////////////////////////////////////////////////// | |
| 290 // TabGtk, TabRendererGtk overrides: | |
| 291 | |
| 292 bool TabGtk::IsSelected() const { | |
| 293 return delegate_->IsTabSelected(this); | |
| 294 } | |
| 295 | |
| 296 bool TabGtk::IsVisible() const { | |
| 297 return GTK_WIDGET_FLAGS(event_box_) & GTK_VISIBLE; | |
| 298 } | |
| 299 | |
| 300 void TabGtk::SetVisible(bool visible) const { | |
| 301 if (visible) { | |
| 302 gtk_widget_show(event_box_); | |
| 303 } else { | |
| 304 gtk_widget_hide(event_box_); | |
| 305 } | |
| 306 } | |
| 307 | |
| 308 void TabGtk::CloseButtonClicked() { | |
| 309 delegate_->CloseTab(this); | |
| 310 } | |
| 311 | |
| 312 void TabGtk::UpdateData(TabContents* contents, bool app, bool loading_only) { | |
| 313 TabRendererGtk::UpdateData(contents, app, loading_only); | |
| 314 // Cache the title width so we don't recalculate it every time the tab is | |
| 315 // resized. | |
| 316 title_width_ = GetTitleWidth(title_font(), GetTitle()); | |
| 317 UpdateTooltipState(); | |
| 318 } | |
| 319 | |
| 320 void TabGtk::SetBounds(const gfx::Rect& bounds) { | |
| 321 TabRendererGtk::SetBounds(bounds); | |
| 322 UpdateTooltipState(); | |
| 323 } | |
| 324 | |
| 325 /////////////////////////////////////////////////////////////////////////////// | |
| 326 // TabGtk, private: | |
| 327 | |
| 328 void TabGtk::ShowContextMenu() { | |
| 329 menu_controller_.reset(new ContextMenuController(this)); | |
| 330 menu_controller_->RunMenu(); | |
| 331 } | |
| 332 | |
| 333 void TabGtk::ContextMenuClosed() { | |
| 334 delegate()->StopAllHighlighting(); | |
| 335 menu_controller_.reset(); | |
| 336 } | |
| 337 | |
| 338 void TabGtk::UpdateTooltipState() { | |
| 339 // Only show the tooltip if the title is truncated. | |
| 340 if (title_width_ > title_bounds().width()) { | |
| 341 gtk_widget_set_tooltip_text(widget(), UTF16ToUTF8(GetTitle()).c_str()); | |
| 342 } else { | |
| 343 gtk_widget_set_has_tooltip(widget(), FALSE); | |
| 344 } | |
| 345 } | |
| 346 | |
| 347 void TabGtk::CreateDragWidget() { | |
| 348 DCHECK(!drag_widget_); | |
| 349 drag_widget_ = gtk_invisible_new(); | |
| 350 g_signal_connect(drag_widget_, "drag-failed", | |
| 351 G_CALLBACK(OnDragFailedThunk), this); | |
| 352 g_signal_connect(drag_widget_, "button-release-event", | |
| 353 G_CALLBACK(OnDragButtonReleasedThunk), this); | |
| 354 g_signal_connect_after(drag_widget_, "drag-begin", | |
| 355 G_CALLBACK(OnDragBeginThunk), this); | |
| 356 } | |
| 357 | |
| 358 void TabGtk::DestroyDragWidget() { | |
| 359 if (drag_widget_) { | |
| 360 gtk_widget_destroy(drag_widget_); | |
| 361 drag_widget_ = NULL; | |
| 362 } | |
| 363 } | |
| 364 | |
| 365 void TabGtk::StartDragging(gfx::Point drag_offset) { | |
| 366 CreateDragWidget(); | |
| 367 | |
| 368 GtkTargetList* list = gtk_dnd_util::GetTargetListFromCodeMask( | |
| 369 gtk_dnd_util::CHROME_TAB); | |
| 370 gtk_drag_begin(drag_widget_, list, GDK_ACTION_MOVE, | |
| 371 1, // Drags are always initiated by the left button. | |
| 372 last_mouse_down_); | |
| 373 | |
| 374 delegate_->MaybeStartDrag(this, drag_offset); | |
| 375 } | |
| 376 | |
| 377 void TabGtk::EndDrag(bool canceled) { | |
| 378 // Make sure we only run EndDrag once by canceling any tasks that want | |
| 379 // to call EndDrag. | |
| 380 drag_end_factory_.RevokeAll(); | |
| 381 | |
| 382 // We must let gtk clean up after we handle the drag operation, otherwise | |
| 383 // there will be outstanding references to the drag widget when we try to | |
| 384 // destroy it. | |
| 385 MessageLoop::current()->PostTask(FROM_HERE, | |
| 386 destroy_factory_.NewRunnableMethod(&TabGtk::DestroyDragWidget)); | |
| 387 | |
| 388 if (last_mouse_down_) { | |
| 389 gdk_event_free(last_mouse_down_); | |
| 390 last_mouse_down_ = NULL; | |
| 391 } | |
| 392 | |
| 393 // Notify the drag helper that we're done with any potential drag operations. | |
| 394 // Clean up the drag helper, which is re-created on the next mouse press. | |
| 395 delegate_->EndDrag(canceled); | |
| 396 | |
| 397 observer_.reset(); | |
| 398 } | |
| OLD | NEW |