| OLD | NEW |
| (Empty) |
| 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/font_list.h" | |
| 25 #include "ui/gfx/path.h" | |
| 26 #include "ui/gfx/text_utils.h" | |
| 27 | |
| 28 using content::WebContents; | |
| 29 | |
| 30 namespace { | |
| 31 | |
| 32 // Returns the width of the title for the current font, in pixels. | |
| 33 int GetTitleWidth(gfx::Font* font, base::string16 title) { | |
| 34 DCHECK(font); | |
| 35 if (title.empty()) | |
| 36 return 0; | |
| 37 | |
| 38 return gfx::GetStringWidth(title, gfx::FontList(*font)); | |
| 39 } | |
| 40 | |
| 41 } // namespace | |
| 42 | |
| 43 class TabGtk::TabGtkObserverHelper { | |
| 44 public: | |
| 45 explicit TabGtkObserverHelper(TabGtk* tab) | |
| 46 : tab_(tab) { | |
| 47 base::MessageLoopForUI::current()->AddObserver(tab_); | |
| 48 } | |
| 49 | |
| 50 ~TabGtkObserverHelper() { | |
| 51 base::MessageLoopForUI::current()->RemoveObserver(tab_); | |
| 52 } | |
| 53 | |
| 54 private: | |
| 55 TabGtk* tab_; | |
| 56 | |
| 57 DISALLOW_COPY_AND_ASSIGN(TabGtkObserverHelper); | |
| 58 }; | |
| 59 | |
| 60 /////////////////////////////////////////////////////////////////////////////// | |
| 61 // TabGtk, public: | |
| 62 | |
| 63 TabGtk::TabGtk(TabDelegate* delegate) | |
| 64 : TabRendererGtk(delegate->GetThemeProvider()), | |
| 65 delegate_(delegate), | |
| 66 closing_(false), | |
| 67 dragging_(false), | |
| 68 last_mouse_down_(NULL), | |
| 69 drag_widget_(NULL), | |
| 70 title_width_(0), | |
| 71 destroy_factory_(this), | |
| 72 drag_end_factory_(this) { | |
| 73 event_box_ = gtk_input_event_box_new(); | |
| 74 g_signal_connect(event_box_, "button-press-event", | |
| 75 G_CALLBACK(OnButtonPressEventThunk), this); | |
| 76 g_signal_connect(event_box_, "button-release-event", | |
| 77 G_CALLBACK(OnButtonReleaseEventThunk), this); | |
| 78 g_signal_connect(event_box_, "enter-notify-event", | |
| 79 G_CALLBACK(OnEnterNotifyEventThunk), this); | |
| 80 g_signal_connect(event_box_, "leave-notify-event", | |
| 81 G_CALLBACK(OnLeaveNotifyEventThunk), this); | |
| 82 gtk_widget_add_events(event_box_, | |
| 83 GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | | |
| 84 GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK); | |
| 85 gtk_container_add(GTK_CONTAINER(event_box_), TabRendererGtk::widget()); | |
| 86 gtk_widget_show_all(event_box_); | |
| 87 } | |
| 88 | |
| 89 TabGtk::~TabGtk() { | |
| 90 if (drag_widget_) { | |
| 91 // Shadow the drag grab so the grab terminates. We could do this using any | |
| 92 // widget, |drag_widget_| is just convenient. | |
| 93 gtk_grab_add(drag_widget_); | |
| 94 gtk_grab_remove(drag_widget_); | |
| 95 DestroyDragWidget(); | |
| 96 } | |
| 97 | |
| 98 if (menu_controller_.get()) { | |
| 99 // The menu is showing. Close the menu. | |
| 100 menu_controller_->Cancel(); | |
| 101 | |
| 102 // Invoke this so that we hide the highlight. | |
| 103 ContextMenuClosed(); | |
| 104 } | |
| 105 } | |
| 106 | |
| 107 void TabGtk::Raise() const { | |
| 108 UNSHIPPED_TRACE_EVENT0("ui::gtk", "TabGtk::Raise"); | |
| 109 | |
| 110 GdkWindow* window = gtk_input_event_box_get_window( | |
| 111 GTK_INPUT_EVENT_BOX(event_box_)); | |
| 112 gdk_window_raise(window); | |
| 113 TabRendererGtk::Raise(); | |
| 114 } | |
| 115 | |
| 116 gboolean TabGtk::OnButtonPressEvent(GtkWidget* widget, GdkEventButton* event) { | |
| 117 // Every button press ensures either a button-release-event or a drag-fail | |
| 118 // signal for |widget|. | |
| 119 if (event->button == 1 && event->type == GDK_BUTTON_PRESS) { | |
| 120 // Store whether or not we were selected just now... we only want to be | |
| 121 // able to drag foreground tabs, so we don't start dragging the tab if | |
| 122 // it was in the background. | |
| 123 if (!IsActive()) { | |
| 124 if (event->state & GDK_CONTROL_MASK) | |
| 125 delegate_->ToggleTabSelection(this); | |
| 126 else if (event->state & GDK_SHIFT_MASK) | |
| 127 delegate_->ExtendTabSelection(this); | |
| 128 else | |
| 129 delegate_->ActivateTab(this); | |
| 130 } | |
| 131 // Hook into the message loop to handle dragging. | |
| 132 observer_.reset(new TabGtkObserverHelper(this)); | |
| 133 | |
| 134 // Store the button press event, used to initiate a drag. | |
| 135 last_mouse_down_ = gdk_event_copy(reinterpret_cast<GdkEvent*>(event)); | |
| 136 } else if (event->button == 3) { | |
| 137 // Only show the context menu if the left mouse button isn't down (i.e., | |
| 138 // the user might want to drag instead). | |
| 139 if (!last_mouse_down_) { | |
| 140 menu_controller_.reset(delegate()->GetTabStripMenuControllerForTab(this)); | |
| 141 menu_controller_->RunMenu(gfx::Point(event->x_root, event->y_root), | |
| 142 event->time); | |
| 143 } | |
| 144 } | |
| 145 | |
| 146 return TRUE; | |
| 147 } | |
| 148 | |
| 149 gboolean TabGtk::OnButtonReleaseEvent(GtkWidget* widget, | |
| 150 GdkEventButton* event) { | |
| 151 if (event->button == 1) { | |
| 152 if (IsActive() && !(event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK))) { | |
| 153 delegate_->ActivateTab(this); | |
| 154 } | |
| 155 observer_.reset(); | |
| 156 | |
| 157 if (last_mouse_down_) { | |
| 158 gdk_event_free(last_mouse_down_); | |
| 159 last_mouse_down_ = NULL; | |
| 160 } | |
| 161 } | |
| 162 | |
| 163 GtkAllocation allocation; | |
| 164 gtk_widget_get_allocation(widget, &allocation); | |
| 165 | |
| 166 // Middle mouse up means close the tab, but only if the mouse is over it | |
| 167 // (like a button). | |
| 168 if (event->button == 2 && | |
| 169 event->x >= 0 && event->y >= 0 && | |
| 170 event->x < allocation.width && | |
| 171 event->y < allocation.height) { | |
| 172 // If the user is currently holding the left mouse button down but hasn't | |
| 173 // moved the mouse yet, a drag hasn't started yet. In that case, clean up | |
| 174 // some state before closing the tab to avoid a crash. Once the drag has | |
| 175 // started, we don't get the middle mouse click here. | |
| 176 if (last_mouse_down_) { | |
| 177 DCHECK(!drag_widget_); | |
| 178 observer_.reset(); | |
| 179 gdk_event_free(last_mouse_down_); | |
| 180 last_mouse_down_ = NULL; | |
| 181 } | |
| 182 delegate_->CloseTab(this); | |
| 183 } | |
| 184 | |
| 185 return TRUE; | |
| 186 } | |
| 187 | |
| 188 gboolean TabGtk::OnDragFailed(GtkWidget* widget, GdkDragContext* context, | |
| 189 GtkDragResult result) { | |
| 190 bool canceled = (result == GTK_DRAG_RESULT_USER_CANCELLED); | |
| 191 EndDrag(canceled); | |
| 192 return TRUE; | |
| 193 } | |
| 194 | |
| 195 gboolean TabGtk::OnDragButtonReleased(GtkWidget* widget, | |
| 196 GdkEventButton* button) { | |
| 197 // We always get this event when gtk is releasing the grab and ending the | |
| 198 // drag. However, if the user ended the drag with space or enter, we don't | |
| 199 // get a follow up event to tell us the drag has finished (either a | |
| 200 // drag-failed or a drag-end). So we post a task to manually end the drag. | |
| 201 // If GTK+ does send the drag-failed or drag-end event, we cancel the task. | |
| 202 base::MessageLoop::current()->PostTask( | |
| 203 FROM_HERE, | |
| 204 base::Bind(&TabGtk::EndDrag, drag_end_factory_.GetWeakPtr(), false)); | |
| 205 return TRUE; | |
| 206 } | |
| 207 | |
| 208 void TabGtk::OnDragBegin(GtkWidget* widget, GdkDragContext* context) { | |
| 209 GdkPixbuf* pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, TRUE, 8, 1, 1); | |
| 210 gdk_pixbuf_fill(pixbuf, 0); | |
| 211 gtk_drag_set_icon_pixbuf(context, pixbuf, 0, 0); | |
| 212 g_object_unref(pixbuf); | |
| 213 } | |
| 214 | |
| 215 /////////////////////////////////////////////////////////////////////////////// | |
| 216 // TabGtk, MessageLoop::Observer implementation: | |
| 217 | |
| 218 void TabGtk::WillProcessEvent(GdkEvent* event) { | |
| 219 // Nothing to do. | |
| 220 } | |
| 221 | |
| 222 void TabGtk::DidProcessEvent(GdkEvent* event) { | |
| 223 if (!(event->type == GDK_MOTION_NOTIFY || event->type == GDK_LEAVE_NOTIFY || | |
| 224 event->type == GDK_ENTER_NOTIFY)) { | |
| 225 return; | |
| 226 } | |
| 227 | |
| 228 if (drag_widget_) { | |
| 229 delegate_->ContinueDrag(NULL); | |
| 230 return; | |
| 231 } | |
| 232 | |
| 233 gint old_x = static_cast<gint>(last_mouse_down_->button.x_root); | |
| 234 gint old_y = static_cast<gint>(last_mouse_down_->button.y_root); | |
| 235 gdouble new_x; | |
| 236 gdouble new_y; | |
| 237 gdk_event_get_root_coords(event, &new_x, &new_y); | |
| 238 | |
| 239 if (gtk_drag_check_threshold(widget(), old_x, old_y, | |
| 240 static_cast<gint>(new_x), static_cast<gint>(new_y))) { | |
| 241 StartDragging(gfx::Point( | |
| 242 static_cast<int>(last_mouse_down_->button.x), | |
| 243 static_cast<int>(last_mouse_down_->button.y))); | |
| 244 } | |
| 245 } | |
| 246 | |
| 247 /////////////////////////////////////////////////////////////////////////////// | |
| 248 // TabGtk, TabRendererGtk overrides: | |
| 249 | |
| 250 bool TabGtk::IsActive() const { | |
| 251 return delegate_->IsTabActive(this); | |
| 252 } | |
| 253 | |
| 254 bool TabGtk::IsSelected() const { | |
| 255 return delegate_->IsTabSelected(this); | |
| 256 } | |
| 257 | |
| 258 bool TabGtk::IsVisible() const { | |
| 259 return gtk_widget_get_visible(event_box_); | |
| 260 } | |
| 261 | |
| 262 void TabGtk::SetVisible(bool visible) const { | |
| 263 gtk_widget_set_visible(event_box_, visible); | |
| 264 } | |
| 265 | |
| 266 void TabGtk::CloseButtonClicked() { | |
| 267 delegate_->CloseTab(this); | |
| 268 } | |
| 269 | |
| 270 void TabGtk::UpdateData(WebContents* contents, bool app, bool loading_only) { | |
| 271 TabRendererGtk::UpdateData(contents, app, loading_only); | |
| 272 // Cache the title width so we don't recalculate it every time the tab is | |
| 273 // resized. | |
| 274 title_width_ = GetTitleWidth(title_font(), GetTitle()); | |
| 275 UpdateTooltipState(); | |
| 276 } | |
| 277 | |
| 278 void TabGtk::SetBounds(const gfx::Rect& bounds) { | |
| 279 TabRendererGtk::SetBounds(bounds); | |
| 280 | |
| 281 if (gtk_input_event_box_get_window(GTK_INPUT_EVENT_BOX(event_box_))) { | |
| 282 gfx::Path mask; | |
| 283 TabResources::GetHitTestMask(bounds.width(), bounds.height(), false, &mask); | |
| 284 ui::ScopedRegion region(mask.CreateNativeRegion()); | |
| 285 gdk_window_input_shape_combine_region( | |
| 286 gtk_input_event_box_get_window(GTK_INPUT_EVENT_BOX(event_box_)), | |
| 287 region.Get(), | |
| 288 0, 0); | |
| 289 } | |
| 290 | |
| 291 UpdateTooltipState(); | |
| 292 } | |
| 293 | |
| 294 /////////////////////////////////////////////////////////////////////////////// | |
| 295 // TabGtk, private: | |
| 296 | |
| 297 void TabGtk::ContextMenuClosed() { | |
| 298 delegate()->StopAllHighlighting(); | |
| 299 menu_controller_.reset(); | |
| 300 } | |
| 301 | |
| 302 void TabGtk::UpdateTooltipState() { | |
| 303 // Note: This method can be called several times per second for various | |
| 304 // reasons (e.g., navigation/loading state changes, tab media indicator | |
| 305 // updates, and so on). However, we must avoid calling the | |
| 306 // gtk_widget_set_XXX() methods if there is no actual change in state. | |
| 307 // Otherwise, GTK will continuously re-hide *all* tooltips throughout the | |
| 308 // browser in response! http://crbug.com/333002 | |
| 309 | |
| 310 // Only show the tooltip if the title is truncated. | |
| 311 if (title_width_ > title_bounds().width()) { | |
| 312 const std::string utf8_title = base::UTF16ToUTF8(GetTitle()); | |
| 313 if (gtk_widget_get_has_tooltip(widget())) { | |
| 314 gchar* const current_tooltip = gtk_widget_get_tooltip_text(widget()); | |
| 315 if (current_tooltip) { | |
| 316 const bool title_unchanged = (utf8_title == current_tooltip); | |
| 317 g_free(current_tooltip); | |
| 318 if (title_unchanged) | |
| 319 return; | |
| 320 } | |
| 321 } | |
| 322 gtk_widget_set_tooltip_text(widget(), utf8_title.c_str()); | |
| 323 } else { | |
| 324 if (gtk_widget_get_has_tooltip(widget())) | |
| 325 gtk_widget_set_has_tooltip(widget(), FALSE); | |
| 326 } | |
| 327 } | |
| 328 | |
| 329 void TabGtk::CreateDragWidget() { | |
| 330 DCHECK(!drag_widget_); | |
| 331 drag_widget_ = gtk_invisible_new(); | |
| 332 g_signal_connect(drag_widget_, "drag-failed", | |
| 333 G_CALLBACK(OnDragFailedThunk), this); | |
| 334 g_signal_connect(drag_widget_, "button-release-event", | |
| 335 G_CALLBACK(OnDragButtonReleasedThunk), this); | |
| 336 g_signal_connect_after(drag_widget_, "drag-begin", | |
| 337 G_CALLBACK(OnDragBeginThunk), this); | |
| 338 } | |
| 339 | |
| 340 void TabGtk::DestroyDragWidget() { | |
| 341 if (drag_widget_) { | |
| 342 gtk_widget_destroy(drag_widget_); | |
| 343 drag_widget_ = NULL; | |
| 344 } | |
| 345 } | |
| 346 | |
| 347 void TabGtk::StartDragging(gfx::Point drag_offset) { | |
| 348 // If the drag is processed after the selection change it's possible | |
| 349 // that the tab has been deselected, in which case we don't want to drag. | |
| 350 if (!IsSelected()) | |
| 351 return; | |
| 352 | |
| 353 CreateDragWidget(); | |
| 354 | |
| 355 GtkTargetList* list = ui::GetTargetListFromCodeMask(ui::CHROME_TAB); | |
| 356 gtk_drag_begin(drag_widget_, list, GDK_ACTION_MOVE, | |
| 357 1, // Drags are always initiated by the left button. | |
| 358 last_mouse_down_); | |
| 359 // gtk_drag_begin adds a reference to list, so unref it here. | |
| 360 gtk_target_list_unref(list); | |
| 361 delegate_->MaybeStartDrag(this, drag_offset); | |
| 362 } | |
| 363 | |
| 364 void TabGtk::EndDrag(bool canceled) { | |
| 365 // Make sure we only run EndDrag once by canceling any tasks that want | |
| 366 // to call EndDrag. | |
| 367 drag_end_factory_.InvalidateWeakPtrs(); | |
| 368 | |
| 369 // We must let gtk clean up after we handle the drag operation, otherwise | |
| 370 // there will be outstanding references to the drag widget when we try to | |
| 371 // destroy it. | |
| 372 base::MessageLoop::current()->PostTask( | |
| 373 FROM_HERE, | |
| 374 base::Bind(&TabGtk::DestroyDragWidget, destroy_factory_.GetWeakPtr())); | |
| 375 | |
| 376 if (last_mouse_down_) { | |
| 377 gdk_event_free(last_mouse_down_); | |
| 378 last_mouse_down_ = NULL; | |
| 379 } | |
| 380 | |
| 381 // Notify the drag helper that we're done with any potential drag operations. | |
| 382 // Clean up the drag helper, which is re-created on the next mouse press. | |
| 383 delegate_->EndDrag(canceled); | |
| 384 | |
| 385 observer_.reset(); | |
| 386 } | |
| OLD | NEW |