| 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/browser_actions_toolbar_gtk.h" | |
| 6 | |
| 7 #include <vector> | |
| 8 | |
| 9 #include "base/i18n/rtl.h" | |
| 10 #include "base/utf_string_conversions.h" | |
| 11 #include "chrome/browser/extensions/extension_browser_event_router.h" | |
| 12 #include "chrome/browser/extensions/extension_context_menu_model.h" | |
| 13 #include "chrome/browser/extensions/extension_service.h" | |
| 14 #include "chrome/browser/extensions/image_loading_tracker.h" | |
| 15 #include "chrome/browser/gtk/cairo_cached_surface.h" | |
| 16 #include "chrome/browser/gtk/extension_popup_gtk.h" | |
| 17 #include "chrome/browser/gtk/gtk_chrome_button.h" | |
| 18 #include "chrome/browser/gtk/gtk_chrome_shrinkable_hbox.h" | |
| 19 #include "chrome/browser/gtk/gtk_theme_provider.h" | |
| 20 #include "chrome/browser/gtk/gtk_util.h" | |
| 21 #include "chrome/browser/gtk/hover_controller_gtk.h" | |
| 22 #include "chrome/browser/gtk/menu_gtk.h" | |
| 23 #include "chrome/browser/gtk/view_id_util.h" | |
| 24 #include "chrome/browser/profiles/profile.h" | |
| 25 #include "chrome/browser/tab_contents/tab_contents.h" | |
| 26 #include "chrome/browser/ui/browser.h" | |
| 27 #include "chrome/common/extensions/extension.h" | |
| 28 #include "chrome/common/extensions/extension_action.h" | |
| 29 #include "chrome/common/extensions/extension_resource.h" | |
| 30 #include "chrome/common/notification_details.h" | |
| 31 #include "chrome/common/notification_service.h" | |
| 32 #include "chrome/common/notification_source.h" | |
| 33 #include "chrome/common/notification_type.h" | |
| 34 #include "gfx/canvas_skia_paint.h" | |
| 35 #include "gfx/gtk_util.h" | |
| 36 #include "grit/app_resources.h" | |
| 37 #include "grit/theme_resources.h" | |
| 38 | |
| 39 namespace { | |
| 40 | |
| 41 // The width of the browser action buttons. | |
| 42 const int kButtonWidth = 27; | |
| 43 | |
| 44 // The padding between browser action buttons. | |
| 45 const int kButtonPadding = 4; | |
| 46 | |
| 47 // The padding to the right of the browser action buttons (between the buttons | |
| 48 // and chevron if they are both showing). | |
| 49 const int kButtonChevronPadding = 2; | |
| 50 | |
| 51 // The padding to the left, top and bottom of the browser actions toolbar | |
| 52 // separator. | |
| 53 const int kSeparatorPadding = 2; | |
| 54 | |
| 55 // Width of the invisible gripper for resizing the toolbar. | |
| 56 const int kResizeGripperWidth = 4; | |
| 57 | |
| 58 const char* kDragTarget = "application/x-chrome-browseraction"; | |
| 59 | |
| 60 GtkTargetEntry GetDragTargetEntry() { | |
| 61 static std::string drag_target_string(kDragTarget); | |
| 62 GtkTargetEntry drag_target; | |
| 63 drag_target.target = const_cast<char*>(drag_target_string.c_str()); | |
| 64 drag_target.flags = GTK_TARGET_SAME_APP; | |
| 65 drag_target.info = 0; | |
| 66 return drag_target; | |
| 67 } | |
| 68 | |
| 69 // The minimum width in pixels of the button hbox if |icon_count| icons are | |
| 70 // showing. | |
| 71 gint WidthForIconCount(gint icon_count) { | |
| 72 return std::max((kButtonWidth + kButtonPadding) * icon_count - kButtonPadding, | |
| 73 0); | |
| 74 } | |
| 75 | |
| 76 } // namespace | |
| 77 | |
| 78 using menus::SimpleMenuModel; | |
| 79 | |
| 80 class BrowserActionButton : public NotificationObserver, | |
| 81 public ImageLoadingTracker::Observer, | |
| 82 public ExtensionContextMenuModel::PopupDelegate, | |
| 83 public MenuGtk::Delegate { | |
| 84 public: | |
| 85 BrowserActionButton(BrowserActionsToolbarGtk* toolbar, | |
| 86 const Extension* extension, | |
| 87 GtkThemeProvider* theme_provider) | |
| 88 : toolbar_(toolbar), | |
| 89 extension_(extension), | |
| 90 image_(NULL), | |
| 91 tracker_(this), | |
| 92 tab_specific_icon_(NULL), | |
| 93 default_icon_(NULL) { | |
| 94 button_.reset(new CustomDrawButton( | |
| 95 theme_provider, | |
| 96 IDR_BROWSER_ACTION, | |
| 97 IDR_BROWSER_ACTION_P, | |
| 98 IDR_BROWSER_ACTION_H, | |
| 99 0, | |
| 100 NULL)); | |
| 101 alignment_.Own(gtk_alignment_new(0, 0, 1, 1)); | |
| 102 gtk_container_add(GTK_CONTAINER(alignment_.get()), button()); | |
| 103 gtk_widget_show(button()); | |
| 104 | |
| 105 DCHECK(extension_->browser_action()); | |
| 106 | |
| 107 UpdateState(); | |
| 108 | |
| 109 // The Browser Action API does not allow the default icon path to be | |
| 110 // changed at runtime, so we can load this now and cache it. | |
| 111 std::string path = extension_->browser_action()->default_icon_path(); | |
| 112 if (!path.empty()) { | |
| 113 tracker_.LoadImage(extension_, extension_->GetResource(path), | |
| 114 gfx::Size(Extension::kBrowserActionIconMaxSize, | |
| 115 Extension::kBrowserActionIconMaxSize), | |
| 116 ImageLoadingTracker::DONT_CACHE); | |
| 117 } | |
| 118 | |
| 119 signals_.Connect(button(), "button-press-event", | |
| 120 G_CALLBACK(OnButtonPress), this); | |
| 121 signals_.Connect(button(), "clicked", | |
| 122 G_CALLBACK(OnClicked), this); | |
| 123 signals_.Connect(button(), "drag-begin", | |
| 124 G_CALLBACK(&OnDragBegin), this); | |
| 125 signals_.ConnectAfter(widget(), "expose-event", | |
| 126 G_CALLBACK(OnExposeEvent), this); | |
| 127 | |
| 128 registrar_.Add(this, NotificationType::EXTENSION_BROWSER_ACTION_UPDATED, | |
| 129 Source<ExtensionAction>(extension->browser_action())); | |
| 130 } | |
| 131 | |
| 132 ~BrowserActionButton() { | |
| 133 if (tab_specific_icon_) | |
| 134 g_object_unref(tab_specific_icon_); | |
| 135 | |
| 136 if (default_icon_) | |
| 137 g_object_unref(default_icon_); | |
| 138 | |
| 139 alignment_.Destroy(); | |
| 140 } | |
| 141 | |
| 142 GtkWidget* button() { return button_->widget(); } | |
| 143 | |
| 144 GtkWidget* widget() { return alignment_.get(); } | |
| 145 | |
| 146 const Extension* extension() { return extension_; } | |
| 147 | |
| 148 // NotificationObserver implementation. | |
| 149 void Observe(NotificationType type, | |
| 150 const NotificationSource& source, | |
| 151 const NotificationDetails& details) { | |
| 152 if (type == NotificationType::EXTENSION_BROWSER_ACTION_UPDATED) | |
| 153 UpdateState(); | |
| 154 else | |
| 155 NOTREACHED(); | |
| 156 } | |
| 157 | |
| 158 // ImageLoadingTracker::Observer implementation. | |
| 159 void OnImageLoaded(SkBitmap* image, ExtensionResource resource, int index) { | |
| 160 if (image) { | |
| 161 default_skbitmap_ = *image; | |
| 162 default_icon_ = gfx::GdkPixbufFromSkBitmap(image); | |
| 163 } | |
| 164 UpdateState(); | |
| 165 } | |
| 166 | |
| 167 // Updates the button based on the latest state from the associated | |
| 168 // browser action. | |
| 169 void UpdateState() { | |
| 170 int tab_id = toolbar_->GetCurrentTabId(); | |
| 171 if (tab_id < 0) | |
| 172 return; | |
| 173 | |
| 174 std::string tooltip = extension_->browser_action()->GetTitle(tab_id); | |
| 175 if (tooltip.empty()) | |
| 176 gtk_widget_set_has_tooltip(button(), FALSE); | |
| 177 else | |
| 178 gtk_widget_set_tooltip_text(button(), tooltip.c_str()); | |
| 179 | |
| 180 SkBitmap image = extension_->browser_action()->GetIcon(tab_id); | |
| 181 if (!image.isNull()) { | |
| 182 GdkPixbuf* previous_gdk_icon = tab_specific_icon_; | |
| 183 tab_specific_icon_ = gfx::GdkPixbufFromSkBitmap(&image); | |
| 184 SetImage(tab_specific_icon_); | |
| 185 if (previous_gdk_icon) | |
| 186 g_object_unref(previous_gdk_icon); | |
| 187 } else if (default_icon_) { | |
| 188 SetImage(default_icon_); | |
| 189 } | |
| 190 gtk_widget_queue_draw(button()); | |
| 191 } | |
| 192 | |
| 193 SkBitmap GetIcon() { | |
| 194 const SkBitmap& image = extension_->browser_action()->GetIcon( | |
| 195 toolbar_->GetCurrentTabId()); | |
| 196 if (!image.isNull()) { | |
| 197 return image; | |
| 198 } else { | |
| 199 return default_skbitmap_; | |
| 200 } | |
| 201 } | |
| 202 | |
| 203 MenuGtk* GetContextMenu() { | |
| 204 context_menu_model_ = | |
| 205 new ExtensionContextMenuModel(extension_, toolbar_->browser(), this); | |
| 206 context_menu_.reset( | |
| 207 new MenuGtk(this, context_menu_model_.get())); | |
| 208 return context_menu_.get(); | |
| 209 } | |
| 210 | |
| 211 private: | |
| 212 // MenuGtk::Delegate implementation. | |
| 213 virtual void StoppedShowing() { | |
| 214 button_->UnsetPaintOverride(); | |
| 215 | |
| 216 // If the context menu was showing for the overflow menu, re-assert the | |
| 217 // grab that was shadowed. | |
| 218 if (toolbar_->overflow_menu_.get()) | |
| 219 gtk_util::GrabAllInput(toolbar_->overflow_menu_->widget()); | |
| 220 } | |
| 221 | |
| 222 virtual void CommandWillBeExecuted() { | |
| 223 // If the context menu was showing for the overflow menu, and a command | |
| 224 // is executed, then stop showing the overflow menu. | |
| 225 if (toolbar_->overflow_menu_.get()) | |
| 226 toolbar_->overflow_menu_->Cancel(); | |
| 227 } | |
| 228 | |
| 229 // Returns true to prevent further processing of the event that caused us to | |
| 230 // show the popup, or false to continue processing. | |
| 231 bool ShowPopup(bool devtools) { | |
| 232 ExtensionAction* browser_action = extension_->browser_action(); | |
| 233 | |
| 234 int tab_id = toolbar_->GetCurrentTabId(); | |
| 235 if (tab_id < 0) { | |
| 236 NOTREACHED() << "No current tab."; | |
| 237 return true; | |
| 238 } | |
| 239 | |
| 240 if (browser_action->HasPopup(tab_id)) { | |
| 241 ExtensionPopupGtk::Show( | |
| 242 browser_action->GetPopupUrl(tab_id), toolbar_->browser(), | |
| 243 widget(), devtools); | |
| 244 return true; | |
| 245 } | |
| 246 | |
| 247 return false; | |
| 248 } | |
| 249 | |
| 250 // ExtensionContextMenuModel::PopupDelegate implementation. | |
| 251 virtual void InspectPopup(ExtensionAction* action) { | |
| 252 ShowPopup(true); | |
| 253 } | |
| 254 | |
| 255 void SetImage(GdkPixbuf* image) { | |
| 256 if (!image_) { | |
| 257 image_ = gtk_image_new_from_pixbuf(image); | |
| 258 gtk_button_set_image(GTK_BUTTON(button()), image_); | |
| 259 } else { | |
| 260 gtk_image_set_from_pixbuf(GTK_IMAGE(image_), image); | |
| 261 } | |
| 262 } | |
| 263 | |
| 264 static gboolean OnButtonPress(GtkWidget* widget, | |
| 265 GdkEvent* event, | |
| 266 BrowserActionButton* action) { | |
| 267 if (event->button.button != 3) | |
| 268 return FALSE; | |
| 269 | |
| 270 action->button_->SetPaintOverride(GTK_STATE_ACTIVE); | |
| 271 action->GetContextMenu()->Popup(widget, event); | |
| 272 | |
| 273 return TRUE; | |
| 274 } | |
| 275 | |
| 276 static void OnClicked(GtkWidget* widget, BrowserActionButton* action) { | |
| 277 if (action->ShowPopup(false)) | |
| 278 return; | |
| 279 | |
| 280 ExtensionBrowserEventRouter::GetInstance()->BrowserActionExecuted( | |
| 281 action->toolbar_->browser()->profile(), action->extension_->id(), | |
| 282 action->toolbar_->browser()); | |
| 283 } | |
| 284 | |
| 285 static gboolean OnExposeEvent(GtkWidget* widget, | |
| 286 GdkEventExpose* event, | |
| 287 BrowserActionButton* button) { | |
| 288 int tab_id = button->toolbar_->GetCurrentTabId(); | |
| 289 if (tab_id < 0) | |
| 290 return FALSE; | |
| 291 | |
| 292 ExtensionAction* action = button->extension_->browser_action(); | |
| 293 if (action->GetBadgeText(tab_id).empty()) | |
| 294 return FALSE; | |
| 295 | |
| 296 gfx::CanvasSkiaPaint canvas(event, false); | |
| 297 gfx::Rect bounding_rect(widget->allocation); | |
| 298 action->PaintBadge(&canvas, bounding_rect, tab_id); | |
| 299 return FALSE; | |
| 300 } | |
| 301 | |
| 302 static void OnDragBegin(GtkWidget* widget, | |
| 303 GdkDragContext* drag_context, | |
| 304 BrowserActionButton* button) { | |
| 305 // Simply pass along the notification to the toolbar. The point of this | |
| 306 // function is to tell the toolbar which BrowserActionButton initiated the | |
| 307 // drag. | |
| 308 button->toolbar_->DragStarted(button, drag_context); | |
| 309 } | |
| 310 | |
| 311 // The toolbar containing this button. | |
| 312 BrowserActionsToolbarGtk* toolbar_; | |
| 313 | |
| 314 // The extension that contains this browser action. | |
| 315 const Extension* extension_; | |
| 316 | |
| 317 // The button for this browser action. | |
| 318 scoped_ptr<CustomDrawButton> button_; | |
| 319 | |
| 320 // The top level widget (parent of |button_|). | |
| 321 OwnedWidgetGtk alignment_; | |
| 322 | |
| 323 // The one image subwidget in |button_|. We keep this out so we don't alter | |
| 324 // the widget hierarchy while changing the button image because changing the | |
| 325 // GTK widget hierarchy invalidates all tooltips and several popular | |
| 326 // extensions change browser action icon in a loop. | |
| 327 GtkWidget* image_; | |
| 328 | |
| 329 // Loads the button's icons for us on the file thread. | |
| 330 ImageLoadingTracker tracker_; | |
| 331 | |
| 332 // If we are displaying a tab-specific icon, it will be here. | |
| 333 GdkPixbuf* tab_specific_icon_; | |
| 334 | |
| 335 // If the browser action has a default icon, it will be here. | |
| 336 GdkPixbuf* default_icon_; | |
| 337 | |
| 338 // Same as |default_icon_|, but stored as SkBitmap. | |
| 339 SkBitmap default_skbitmap_; | |
| 340 | |
| 341 GtkSignalRegistrar signals_; | |
| 342 NotificationRegistrar registrar_; | |
| 343 | |
| 344 // The context menu view and model for this extension action. | |
| 345 scoped_ptr<MenuGtk> context_menu_; | |
| 346 scoped_refptr<ExtensionContextMenuModel> context_menu_model_; | |
| 347 | |
| 348 friend class BrowserActionsToolbarGtk; | |
| 349 }; | |
| 350 | |
| 351 // BrowserActionsToolbarGtk ---------------------------------------------------- | |
| 352 | |
| 353 BrowserActionsToolbarGtk::BrowserActionsToolbarGtk(Browser* browser) | |
| 354 : browser_(browser), | |
| 355 profile_(browser->profile()), | |
| 356 theme_provider_(GtkThemeProvider::GetFrom(browser->profile())), | |
| 357 model_(NULL), | |
| 358 hbox_(gtk_hbox_new(FALSE, 0)), | |
| 359 button_hbox_(gtk_chrome_shrinkable_hbox_new(TRUE, FALSE, kButtonPadding)), | |
| 360 drag_button_(NULL), | |
| 361 drop_index_(-1), | |
| 362 resize_animation_(this), | |
| 363 desired_width_(0), | |
| 364 start_width_(0), | |
| 365 method_factory_(this) { | |
| 366 ExtensionService* extension_service = profile_->GetExtensionService(); | |
| 367 // The |extension_service| can be NULL in Incognito. | |
| 368 if (!extension_service) | |
| 369 return; | |
| 370 | |
| 371 overflow_button_.reset(new CustomDrawButton( | |
| 372 theme_provider_, | |
| 373 IDR_BROWSER_ACTIONS_OVERFLOW, | |
| 374 IDR_BROWSER_ACTIONS_OVERFLOW_P, | |
| 375 IDR_BROWSER_ACTIONS_OVERFLOW_H, | |
| 376 0, | |
| 377 gtk_arrow_new(GTK_ARROW_DOWN, GTK_SHADOW_NONE))); | |
| 378 | |
| 379 GtkWidget* gripper = gtk_button_new(); | |
| 380 gtk_widget_set_size_request(gripper, kResizeGripperWidth, -1); | |
| 381 GTK_WIDGET_UNSET_FLAGS(gripper, GTK_CAN_FOCUS); | |
| 382 gtk_widget_add_events(gripper, GDK_POINTER_MOTION_MASK); | |
| 383 signals_.Connect(gripper, "motion-notify-event", | |
| 384 G_CALLBACK(OnGripperMotionNotifyThunk), this); | |
| 385 signals_.Connect(gripper, "expose-event", | |
| 386 G_CALLBACK(OnGripperExposeThunk), this); | |
| 387 signals_.Connect(gripper, "enter-notify-event", | |
| 388 G_CALLBACK(OnGripperEnterNotifyThunk), this); | |
| 389 signals_.Connect(gripper, "leave-notify-event", | |
| 390 G_CALLBACK(OnGripperLeaveNotifyThunk), this); | |
| 391 signals_.Connect(gripper, "button-release-event", | |
| 392 G_CALLBACK(OnGripperButtonReleaseThunk), this); | |
| 393 signals_.Connect(gripper, "button-press-event", | |
| 394 G_CALLBACK(OnGripperButtonPressThunk), this); | |
| 395 signals_.Connect(chevron(), "button-press-event", | |
| 396 G_CALLBACK(OnOverflowButtonPressThunk), this); | |
| 397 | |
| 398 // |overflow_alignment| adds padding to the right of the browser action | |
| 399 // buttons, but only appears when the overflow menu is showing. | |
| 400 overflow_alignment_ = gtk_alignment_new(0, 0, 1, 1); | |
| 401 gtk_container_add(GTK_CONTAINER(overflow_alignment_), chevron()); | |
| 402 | |
| 403 // |overflow_area_| holds the overflow chevron and the separator, which | |
| 404 // is only shown in GTK+ theme mode. | |
| 405 overflow_area_ = gtk_hbox_new(FALSE, 0); | |
| 406 gtk_box_pack_start(GTK_BOX(overflow_area_), overflow_alignment_, | |
| 407 FALSE, FALSE, 0); | |
| 408 | |
| 409 separator_ = gtk_vseparator_new(); | |
| 410 gtk_box_pack_start(GTK_BOX(overflow_area_), separator_, | |
| 411 FALSE, FALSE, 0); | |
| 412 gtk_widget_set_no_show_all(separator_, TRUE); | |
| 413 | |
| 414 gtk_widget_show_all(overflow_area_); | |
| 415 gtk_widget_set_no_show_all(overflow_area_, TRUE); | |
| 416 | |
| 417 gtk_box_pack_start(GTK_BOX(hbox_.get()), gripper, FALSE, FALSE, 0); | |
| 418 gtk_box_pack_start(GTK_BOX(hbox_.get()), button_hbox_.get(), TRUE, TRUE, 0); | |
| 419 gtk_box_pack_start(GTK_BOX(hbox_.get()), overflow_area_, FALSE, FALSE, 0); | |
| 420 | |
| 421 model_ = extension_service->toolbar_model(); | |
| 422 model_->AddObserver(this); | |
| 423 SetupDrags(); | |
| 424 | |
| 425 if (model_->extensions_initialized()) { | |
| 426 CreateAllButtons(); | |
| 427 SetContainerWidth(); | |
| 428 } | |
| 429 | |
| 430 // We want to connect to "set-focus" on the toplevel window; we have to wait | |
| 431 // until we are added to a toplevel window to do so. | |
| 432 signals_.Connect(widget(), "hierarchy-changed", | |
| 433 G_CALLBACK(OnHierarchyChangedThunk), this); | |
| 434 | |
| 435 ViewIDUtil::SetID(button_hbox_.get(), VIEW_ID_BROWSER_ACTION_TOOLBAR); | |
| 436 | |
| 437 registrar_.Add(this, | |
| 438 NotificationType::BROWSER_THEME_CHANGED, | |
| 439 NotificationService::AllSources()); | |
| 440 theme_provider_->InitThemesFor(this); | |
| 441 } | |
| 442 | |
| 443 BrowserActionsToolbarGtk::~BrowserActionsToolbarGtk() { | |
| 444 if (model_) | |
| 445 model_->RemoveObserver(this); | |
| 446 button_hbox_.Destroy(); | |
| 447 hbox_.Destroy(); | |
| 448 } | |
| 449 | |
| 450 int BrowserActionsToolbarGtk::GetCurrentTabId() { | |
| 451 TabContents* selected_tab = browser_->GetSelectedTabContents(); | |
| 452 if (!selected_tab) | |
| 453 return -1; | |
| 454 | |
| 455 return selected_tab->controller().session_id().id(); | |
| 456 } | |
| 457 | |
| 458 void BrowserActionsToolbarGtk::Update() { | |
| 459 for (ExtensionButtonMap::iterator iter = extension_button_map_.begin(); | |
| 460 iter != extension_button_map_.end(); ++iter) { | |
| 461 iter->second->UpdateState(); | |
| 462 } | |
| 463 } | |
| 464 | |
| 465 void BrowserActionsToolbarGtk::Observe(NotificationType type, | |
| 466 const NotificationSource& source, | |
| 467 const NotificationDetails& details) { | |
| 468 DCHECK(NotificationType::BROWSER_THEME_CHANGED == type); | |
| 469 if (theme_provider_->UseGtkTheme()) | |
| 470 gtk_widget_show(separator_); | |
| 471 else | |
| 472 gtk_widget_hide(separator_); | |
| 473 } | |
| 474 | |
| 475 void BrowserActionsToolbarGtk::SetupDrags() { | |
| 476 GtkTargetEntry drag_target = GetDragTargetEntry(); | |
| 477 gtk_drag_dest_set(button_hbox_.get(), GTK_DEST_DEFAULT_DROP, &drag_target, 1, | |
| 478 GDK_ACTION_MOVE); | |
| 479 | |
| 480 signals_.Connect(button_hbox_.get(), "drag-motion", | |
| 481 G_CALLBACK(OnDragMotionThunk), this); | |
| 482 } | |
| 483 | |
| 484 void BrowserActionsToolbarGtk::CreateAllButtons() { | |
| 485 extension_button_map_.clear(); | |
| 486 | |
| 487 int i = 0; | |
| 488 for (ExtensionList::iterator iter = model_->begin(); | |
| 489 iter != model_->end(); ++iter) { | |
| 490 CreateButtonForExtension(*iter, i++); | |
| 491 } | |
| 492 } | |
| 493 | |
| 494 void BrowserActionsToolbarGtk::SetContainerWidth() { | |
| 495 int showing_actions = model_->GetVisibleIconCount(); | |
| 496 if (showing_actions >= 0) | |
| 497 SetButtonHBoxWidth(WidthForIconCount(showing_actions)); | |
| 498 } | |
| 499 | |
| 500 void BrowserActionsToolbarGtk::CreateButtonForExtension( | |
| 501 const Extension* extension, int index) { | |
| 502 if (!ShouldDisplayBrowserAction(extension)) | |
| 503 return; | |
| 504 | |
| 505 if (profile_->IsOffTheRecord()) | |
| 506 index = model_->OriginalIndexToIncognito(index); | |
| 507 | |
| 508 RemoveButtonForExtension(extension); | |
| 509 linked_ptr<BrowserActionButton> button( | |
| 510 new BrowserActionButton(this, extension, theme_provider_)); | |
| 511 gtk_chrome_shrinkable_hbox_pack_start( | |
| 512 GTK_CHROME_SHRINKABLE_HBOX(button_hbox_.get()), button->widget(), 0); | |
| 513 gtk_box_reorder_child(GTK_BOX(button_hbox_.get()), button->widget(), index); | |
| 514 extension_button_map_[extension->id()] = button; | |
| 515 | |
| 516 GtkTargetEntry drag_target = GetDragTargetEntry(); | |
| 517 gtk_drag_source_set(button->button(), GDK_BUTTON1_MASK, &drag_target, 1, | |
| 518 GDK_ACTION_MOVE); | |
| 519 // We ignore whether the drag was a "success" or "failure" in Gtk's opinion. | |
| 520 signals_.Connect(button->button(), "drag-end", | |
| 521 G_CALLBACK(&OnDragEndThunk), this); | |
| 522 signals_.Connect(button->button(), "drag-failed", | |
| 523 G_CALLBACK(&OnDragFailedThunk), this); | |
| 524 | |
| 525 // Any time a browser action button is shown or hidden we have to update | |
| 526 // the chevron state. | |
| 527 signals_.Connect(button->widget(), "show", | |
| 528 G_CALLBACK(&OnButtonShowOrHideThunk), this); | |
| 529 signals_.Connect(button->widget(), "hide", | |
| 530 G_CALLBACK(&OnButtonShowOrHideThunk), this); | |
| 531 | |
| 532 gtk_widget_show(button->widget()); | |
| 533 | |
| 534 UpdateVisibility(); | |
| 535 } | |
| 536 | |
| 537 GtkWidget* BrowserActionsToolbarGtk::GetBrowserActionWidget( | |
| 538 const Extension* extension) { | |
| 539 ExtensionButtonMap::iterator it = extension_button_map_.find( | |
| 540 extension->id()); | |
| 541 if (it == extension_button_map_.end()) | |
| 542 return NULL; | |
| 543 | |
| 544 return it->second.get()->widget(); | |
| 545 } | |
| 546 | |
| 547 void BrowserActionsToolbarGtk::RemoveButtonForExtension( | |
| 548 const Extension* extension) { | |
| 549 if (extension_button_map_.erase(extension->id())) | |
| 550 UpdateVisibility(); | |
| 551 UpdateChevronVisibility(); | |
| 552 } | |
| 553 | |
| 554 void BrowserActionsToolbarGtk::UpdateVisibility() { | |
| 555 if (button_count() == 0) | |
| 556 gtk_widget_hide(widget()); | |
| 557 else | |
| 558 gtk_widget_show(widget()); | |
| 559 } | |
| 560 | |
| 561 bool BrowserActionsToolbarGtk::ShouldDisplayBrowserAction( | |
| 562 const Extension* extension) { | |
| 563 // Only display incognito-enabled extensions while in incognito mode. | |
| 564 return (!profile_->IsOffTheRecord() || | |
| 565 profile_->GetExtensionService()->IsIncognitoEnabled(extension)); | |
| 566 } | |
| 567 | |
| 568 void BrowserActionsToolbarGtk::HidePopup() { | |
| 569 ExtensionPopupGtk* popup = ExtensionPopupGtk::get_current_extension_popup(); | |
| 570 if (popup) | |
| 571 popup->DestroyPopup(); | |
| 572 } | |
| 573 | |
| 574 void BrowserActionsToolbarGtk::AnimateToShowNIcons(int count) { | |
| 575 desired_width_ = WidthForIconCount(count); | |
| 576 start_width_ = button_hbox_->allocation.width; | |
| 577 resize_animation_.Reset(); | |
| 578 resize_animation_.Show(); | |
| 579 } | |
| 580 | |
| 581 void BrowserActionsToolbarGtk::BrowserActionAdded(const Extension* extension, | |
| 582 int index) { | |
| 583 overflow_menu_.reset(); | |
| 584 | |
| 585 CreateButtonForExtension(extension, index); | |
| 586 | |
| 587 // If we are still initializing the container, don't bother animating. | |
| 588 if (!model_->extensions_initialized()) | |
| 589 return; | |
| 590 | |
| 591 // Animate the addition if we are showing all browser action buttons. | |
| 592 if (!GTK_WIDGET_VISIBLE(overflow_area_)) { | |
| 593 AnimateToShowNIcons(button_count()); | |
| 594 model_->SetVisibleIconCount(button_count()); | |
| 595 } | |
| 596 } | |
| 597 | |
| 598 void BrowserActionsToolbarGtk::BrowserActionRemoved( | |
| 599 const Extension* extension) { | |
| 600 overflow_menu_.reset(); | |
| 601 | |
| 602 if (drag_button_ != NULL) { | |
| 603 // Break the current drag. | |
| 604 gtk_grab_remove(button_hbox_.get()); | |
| 605 } | |
| 606 | |
| 607 RemoveButtonForExtension(extension); | |
| 608 | |
| 609 if (!GTK_WIDGET_VISIBLE(overflow_area_)) { | |
| 610 AnimateToShowNIcons(button_count()); | |
| 611 model_->SetVisibleIconCount(button_count()); | |
| 612 } | |
| 613 } | |
| 614 | |
| 615 void BrowserActionsToolbarGtk::BrowserActionMoved(const Extension* extension, | |
| 616 int index) { | |
| 617 // We initiated this move action, and have already moved the button. | |
| 618 if (drag_button_ != NULL) | |
| 619 return; | |
| 620 | |
| 621 GtkWidget* button_widget = GetBrowserActionWidget(extension); | |
| 622 if (!button_widget) { | |
| 623 if (ShouldDisplayBrowserAction(extension)) | |
| 624 NOTREACHED(); | |
| 625 return; | |
| 626 } | |
| 627 | |
| 628 if (profile_->IsOffTheRecord()) | |
| 629 index = model_->OriginalIndexToIncognito(index); | |
| 630 | |
| 631 gtk_box_reorder_child(GTK_BOX(button_hbox_.get()), button_widget, index); | |
| 632 } | |
| 633 | |
| 634 void BrowserActionsToolbarGtk::ModelLoaded() { | |
| 635 SetContainerWidth(); | |
| 636 } | |
| 637 | |
| 638 void BrowserActionsToolbarGtk::AnimationProgressed( | |
| 639 const ui::Animation* animation) { | |
| 640 int width = start_width_ + (desired_width_ - start_width_) * | |
| 641 animation->GetCurrentValue(); | |
| 642 gtk_widget_set_size_request(button_hbox_.get(), width, -1); | |
| 643 | |
| 644 if (width == desired_width_) | |
| 645 resize_animation_.Reset(); | |
| 646 } | |
| 647 | |
| 648 void BrowserActionsToolbarGtk::AnimationEnded(const ui::Animation* animation) { | |
| 649 gtk_widget_set_size_request(button_hbox_.get(), desired_width_, -1); | |
| 650 UpdateChevronVisibility(); | |
| 651 } | |
| 652 | |
| 653 bool BrowserActionsToolbarGtk::IsCommandIdChecked(int command_id) const { | |
| 654 return false; | |
| 655 } | |
| 656 | |
| 657 bool BrowserActionsToolbarGtk::IsCommandIdEnabled(int command_id) const { | |
| 658 return true; | |
| 659 } | |
| 660 | |
| 661 bool BrowserActionsToolbarGtk::GetAcceleratorForCommandId( | |
| 662 int command_id, | |
| 663 menus::Accelerator* accelerator) { | |
| 664 return false; | |
| 665 } | |
| 666 | |
| 667 void BrowserActionsToolbarGtk::ExecuteCommand(int command_id) { | |
| 668 const Extension* extension = model_->GetExtensionByIndex(command_id); | |
| 669 ExtensionAction* browser_action = extension->browser_action(); | |
| 670 | |
| 671 int tab_id = GetCurrentTabId(); | |
| 672 if (tab_id < 0) { | |
| 673 NOTREACHED() << "No current tab."; | |
| 674 return; | |
| 675 } | |
| 676 | |
| 677 if (browser_action->HasPopup(tab_id)) { | |
| 678 ExtensionPopupGtk::Show( | |
| 679 browser_action->GetPopupUrl(tab_id), browser(), | |
| 680 chevron(), | |
| 681 false); | |
| 682 } else { | |
| 683 ExtensionBrowserEventRouter::GetInstance()->BrowserActionExecuted( | |
| 684 browser()->profile(), extension->id(), browser()); | |
| 685 } | |
| 686 } | |
| 687 | |
| 688 void BrowserActionsToolbarGtk::StoppedShowing() { | |
| 689 overflow_button_->UnsetPaintOverride(); | |
| 690 } | |
| 691 | |
| 692 bool BrowserActionsToolbarGtk::AlwaysShowIconForCmd(int command_id) const { | |
| 693 return true; | |
| 694 } | |
| 695 | |
| 696 void BrowserActionsToolbarGtk::DragStarted(BrowserActionButton* button, | |
| 697 GdkDragContext* drag_context) { | |
| 698 // No representation of the widget following the cursor. | |
| 699 GdkPixbuf* pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, TRUE, 8, 1, 1); | |
| 700 gtk_drag_set_icon_pixbuf(drag_context, pixbuf, 0, 0); | |
| 701 g_object_unref(pixbuf); | |
| 702 | |
| 703 DCHECK(!drag_button_); | |
| 704 drag_button_ = button; | |
| 705 } | |
| 706 | |
| 707 void BrowserActionsToolbarGtk::SetButtonHBoxWidth(int new_width) { | |
| 708 gint max_width = WidthForIconCount(button_count()); | |
| 709 new_width = std::min(max_width, new_width); | |
| 710 new_width = std::max(new_width, 0); | |
| 711 gtk_widget_set_size_request(button_hbox_.get(), new_width, -1); | |
| 712 } | |
| 713 | |
| 714 void BrowserActionsToolbarGtk::UpdateChevronVisibility() { | |
| 715 int showing_icon_count = | |
| 716 gtk_chrome_shrinkable_hbox_get_visible_child_count( | |
| 717 GTK_CHROME_SHRINKABLE_HBOX(button_hbox_.get())); | |
| 718 if (showing_icon_count == 0) { | |
| 719 gtk_alignment_set_padding(GTK_ALIGNMENT(overflow_alignment_), 0, 0, 0, 0); | |
| 720 } else { | |
| 721 gtk_alignment_set_padding(GTK_ALIGNMENT(overflow_alignment_), 0, 0, | |
| 722 kButtonChevronPadding, 0); | |
| 723 } | |
| 724 | |
| 725 if (button_count() > showing_icon_count) { | |
| 726 if (!GTK_WIDGET_VISIBLE(overflow_area_)) { | |
| 727 if (drag_button_) { | |
| 728 // During drags, when the overflow chevron shows for the first time, | |
| 729 // take that much space away from |button_hbox_| to make the drag look | |
| 730 // smoother. | |
| 731 GtkRequisition req; | |
| 732 gtk_widget_size_request(chevron(), &req); | |
| 733 gint overflow_width = req.width; | |
| 734 gtk_widget_size_request(button_hbox_.get(), &req); | |
| 735 gint button_hbox_width = req.width; | |
| 736 button_hbox_width = std::max(button_hbox_width - overflow_width, 0); | |
| 737 gtk_widget_set_size_request(button_hbox_.get(), button_hbox_width, -1); | |
| 738 } | |
| 739 | |
| 740 gtk_widget_show(overflow_area_); | |
| 741 } | |
| 742 } else { | |
| 743 gtk_widget_hide(overflow_area_); | |
| 744 } | |
| 745 } | |
| 746 | |
| 747 gboolean BrowserActionsToolbarGtk::OnDragMotion(GtkWidget* widget, | |
| 748 GdkDragContext* drag_context, | |
| 749 gint x, gint y, guint time) { | |
| 750 // Only handle drags we initiated. | |
| 751 if (!drag_button_) | |
| 752 return FALSE; | |
| 753 | |
| 754 if (base::i18n::IsRTL()) | |
| 755 x = widget->allocation.width - x; | |
| 756 drop_index_ = x < kButtonWidth ? 0 : x / (kButtonWidth + kButtonPadding); | |
| 757 | |
| 758 // We will go ahead and reorder the child in order to provide visual feedback | |
| 759 // to the user. We don't inform the model that it has moved until the drag | |
| 760 // ends. | |
| 761 gtk_box_reorder_child(GTK_BOX(button_hbox_.get()), drag_button_->widget(), | |
| 762 drop_index_); | |
| 763 | |
| 764 gdk_drag_status(drag_context, GDK_ACTION_MOVE, time); | |
| 765 return TRUE; | |
| 766 } | |
| 767 | |
| 768 void BrowserActionsToolbarGtk::OnDragEnd(GtkWidget* button, | |
| 769 GdkDragContext* drag_context) { | |
| 770 if (drop_index_ != -1) { | |
| 771 if (profile_->IsOffTheRecord()) | |
| 772 drop_index_ = model_->IncognitoIndexToOriginal(drop_index_); | |
| 773 | |
| 774 model_->MoveBrowserAction(drag_button_->extension(), drop_index_); | |
| 775 } | |
| 776 | |
| 777 drag_button_ = NULL; | |
| 778 drop_index_ = -1; | |
| 779 } | |
| 780 | |
| 781 gboolean BrowserActionsToolbarGtk::OnDragFailed(GtkWidget* widget, | |
| 782 GdkDragContext* drag_context, | |
| 783 GtkDragResult result) { | |
| 784 // We connect to this signal and return TRUE so that the default failure | |
| 785 // animation (wherein the drag widget floats back to the start of the drag) | |
| 786 // does not show, and the drag-end signal is emitted immediately instead of | |
| 787 // several seconds later. | |
| 788 return TRUE; | |
| 789 } | |
| 790 | |
| 791 void BrowserActionsToolbarGtk::OnHierarchyChanged( | |
| 792 GtkWidget* widget, GtkWidget* previous_toplevel) { | |
| 793 GtkWidget* toplevel = gtk_widget_get_toplevel(widget); | |
| 794 if (!GTK_WIDGET_TOPLEVEL(toplevel)) | |
| 795 return; | |
| 796 | |
| 797 signals_.Connect(toplevel, "set-focus", G_CALLBACK(OnSetFocusThunk), this); | |
| 798 } | |
| 799 | |
| 800 void BrowserActionsToolbarGtk::OnSetFocus(GtkWidget* widget, | |
| 801 GtkWidget* focus_widget) { | |
| 802 ExtensionPopupGtk* popup = ExtensionPopupGtk::get_current_extension_popup(); | |
| 803 // The focus of the parent window has changed. Close the popup. Delay the hide | |
| 804 // because it will destroy the RenderViewHost, which may still be on the | |
| 805 // call stack. | |
| 806 if (!popup || popup->being_inspected()) | |
| 807 return; | |
| 808 MessageLoop::current()->PostTask(FROM_HERE, | |
| 809 method_factory_.NewRunnableMethod(&BrowserActionsToolbarGtk::HidePopup)); | |
| 810 } | |
| 811 | |
| 812 gboolean BrowserActionsToolbarGtk::OnGripperMotionNotify( | |
| 813 GtkWidget* widget, GdkEventMotion* event) { | |
| 814 if (!(event->state & GDK_BUTTON1_MASK)) | |
| 815 return FALSE; | |
| 816 | |
| 817 // Calculate how much the user dragged the gripper and subtract that off the | |
| 818 // button container's width. | |
| 819 int distance_dragged = base::i18n::IsRTL() ? | |
| 820 -event->x : | |
| 821 event->x - widget->allocation.width; | |
| 822 gint new_width = button_hbox_->allocation.width - distance_dragged; | |
| 823 SetButtonHBoxWidth(new_width); | |
| 824 | |
| 825 return FALSE; | |
| 826 } | |
| 827 | |
| 828 gboolean BrowserActionsToolbarGtk::OnGripperExpose(GtkWidget* gripper, | |
| 829 GdkEventExpose* expose) { | |
| 830 return TRUE; | |
| 831 } | |
| 832 | |
| 833 // These three signal handlers (EnterNotify, LeaveNotify, and ButtonRelease) | |
| 834 // are used to give the gripper the resize cursor. Since it doesn't have its | |
| 835 // own window, we have to set the cursor whenever the pointer moves into the | |
| 836 // button or leaves the button, and be sure to leave it on when the user is | |
| 837 // dragging. | |
| 838 gboolean BrowserActionsToolbarGtk::OnGripperEnterNotify( | |
| 839 GtkWidget* gripper, GdkEventCrossing* event) { | |
| 840 gdk_window_set_cursor(gripper->window, | |
| 841 gfx::GetCursor(GDK_SB_H_DOUBLE_ARROW)); | |
| 842 return FALSE; | |
| 843 } | |
| 844 | |
| 845 gboolean BrowserActionsToolbarGtk::OnGripperLeaveNotify( | |
| 846 GtkWidget* gripper, GdkEventCrossing* event) { | |
| 847 if (!(event->state & GDK_BUTTON1_MASK)) | |
| 848 gdk_window_set_cursor(gripper->window, NULL); | |
| 849 return FALSE; | |
| 850 } | |
| 851 | |
| 852 gboolean BrowserActionsToolbarGtk::OnGripperButtonRelease( | |
| 853 GtkWidget* gripper, GdkEventButton* event) { | |
| 854 gfx::Rect gripper_rect(0, 0, | |
| 855 gripper->allocation.width, gripper->allocation.height); | |
| 856 gfx::Point release_point(event->x, event->y); | |
| 857 if (!gripper_rect.Contains(release_point)) | |
| 858 gdk_window_set_cursor(gripper->window, NULL); | |
| 859 | |
| 860 // After the user resizes the toolbar, we want to smartly resize it to be | |
| 861 // the perfect size to fit the buttons. | |
| 862 int visible_icon_count = | |
| 863 gtk_chrome_shrinkable_hbox_get_visible_child_count( | |
| 864 GTK_CHROME_SHRINKABLE_HBOX(button_hbox_.get())); | |
| 865 AnimateToShowNIcons(visible_icon_count); | |
| 866 model_->SetVisibleIconCount(visible_icon_count); | |
| 867 | |
| 868 return FALSE; | |
| 869 } | |
| 870 | |
| 871 gboolean BrowserActionsToolbarGtk::OnGripperButtonPress( | |
| 872 GtkWidget* gripper, GdkEventButton* event) { | |
| 873 resize_animation_.Reset(); | |
| 874 | |
| 875 return FALSE; | |
| 876 } | |
| 877 | |
| 878 gboolean BrowserActionsToolbarGtk::OnOverflowButtonPress( | |
| 879 GtkWidget* overflow, GdkEventButton* event) { | |
| 880 overflow_menu_model_.reset(new SimpleMenuModel(this)); | |
| 881 | |
| 882 int visible_icon_count = | |
| 883 gtk_chrome_shrinkable_hbox_get_visible_child_count( | |
| 884 GTK_CHROME_SHRINKABLE_HBOX(button_hbox_.get())); | |
| 885 for (int i = visible_icon_count; i < button_count(); ++i) { | |
| 886 int model_index = i; | |
| 887 if (profile_->IsOffTheRecord()) | |
| 888 model_index = model_->IncognitoIndexToOriginal(i); | |
| 889 | |
| 890 const Extension* extension = model_->GetExtensionByIndex(model_index); | |
| 891 BrowserActionButton* button = extension_button_map_[extension->id()].get(); | |
| 892 | |
| 893 overflow_menu_model_->AddItem(model_index, UTF8ToUTF16(extension->name())); | |
| 894 overflow_menu_model_->SetIcon(overflow_menu_model_->GetItemCount() - 1, | |
| 895 button->GetIcon()); | |
| 896 | |
| 897 // TODO(estade): set the menu item's tooltip. | |
| 898 } | |
| 899 | |
| 900 overflow_menu_.reset(new MenuGtk(this, overflow_menu_model_.get())); | |
| 901 signals_.Connect(overflow_menu_->widget(), "button-press-event", | |
| 902 G_CALLBACK(OnOverflowMenuButtonPressThunk), this); | |
| 903 | |
| 904 overflow_button_->SetPaintOverride(GTK_STATE_ACTIVE); | |
| 905 overflow_menu_->PopupAsFromKeyEvent(chevron()); | |
| 906 | |
| 907 return FALSE; | |
| 908 } | |
| 909 | |
| 910 gboolean BrowserActionsToolbarGtk::OnOverflowMenuButtonPress( | |
| 911 GtkWidget* overflow, GdkEventButton* event) { | |
| 912 if (event->button != 3) | |
| 913 return FALSE; | |
| 914 | |
| 915 GtkWidget* menu_item = GTK_MENU_SHELL(overflow)->active_menu_item; | |
| 916 if (!menu_item) | |
| 917 return FALSE; | |
| 918 | |
| 919 int item_index = g_list_index(GTK_MENU_SHELL(overflow)->children, menu_item); | |
| 920 if (item_index == -1) { | |
| 921 NOTREACHED(); | |
| 922 return FALSE; | |
| 923 } | |
| 924 | |
| 925 item_index += gtk_chrome_shrinkable_hbox_get_visible_child_count( | |
| 926 GTK_CHROME_SHRINKABLE_HBOX(button_hbox_.get())); | |
| 927 if (profile_->IsOffTheRecord()) | |
| 928 item_index = model_->IncognitoIndexToOriginal(item_index); | |
| 929 | |
| 930 const Extension* extension = model_->GetExtensionByIndex(item_index); | |
| 931 ExtensionButtonMap::iterator it = extension_button_map_.find( | |
| 932 extension->id()); | |
| 933 if (it == extension_button_map_.end()) { | |
| 934 NOTREACHED(); | |
| 935 return FALSE; | |
| 936 } | |
| 937 | |
| 938 it->second.get()->GetContextMenu()->PopupAsContext(event->time); | |
| 939 return TRUE; | |
| 940 } | |
| 941 | |
| 942 void BrowserActionsToolbarGtk::OnButtonShowOrHide(GtkWidget* sender) { | |
| 943 if (!resize_animation_.is_animating()) | |
| 944 UpdateChevronVisibility(); | |
| 945 } | |
| OLD | NEW |