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/browser_actions_toolbar_gtk.h" 6 7 #include <gtk/gtk.h> 8 9 #include <algorithm> 10 #include <vector> 11 12 #include "base/bind.h" 13 #include "base/i18n/rtl.h" 14 #include "base/message_loop/message_loop.h" 15 #include "base/strings/utf_string_conversions.h" 16 #include "chrome/browser/chrome_notification_types.h" 17 #include "chrome/browser/extensions/api/commands/command_service.h" 18 #include "chrome/browser/extensions/extension_action.h" 19 #include "chrome/browser/extensions/extension_action_icon_factory.h" 20 #include "chrome/browser/extensions/extension_action_manager.h" 21 #include "chrome/browser/extensions/extension_context_menu_model.h" 22 #include "chrome/browser/extensions/extension_service.h" 23 #include "chrome/browser/extensions/extension_system.h" 24 #include "chrome/browser/profiles/profile.h" 25 #include "chrome/browser/sessions/session_tab_helper.h" 26 #include "chrome/browser/ui/browser.h" 27 #include "chrome/browser/ui/gtk/browser_window_gtk.h" 28 #include "chrome/browser/ui/gtk/custom_button.h" 29 #include "chrome/browser/ui/gtk/extensions/extension_popup_gtk.h" 30 #include "chrome/browser/ui/gtk/gtk_chrome_button.h" 31 #include "chrome/browser/ui/gtk/gtk_chrome_shrinkable_hbox.h" 32 #include "chrome/browser/ui/gtk/gtk_theme_service.h" 33 #include "chrome/browser/ui/gtk/gtk_util.h" 34 #include "chrome/browser/ui/gtk/hover_controller_gtk.h" 35 #include "chrome/browser/ui/gtk/menu_gtk.h" 36 #include "chrome/browser/ui/gtk/view_id_util.h" 37 #include "chrome/browser/ui/tabs/tab_strip_model.h" 38 #include "chrome/common/extensions/extension.h" 39 #include "chrome/common/extensions/extension_manifest_constants.h" 40 #include "content/public/browser/notification_details.h" 41 #include "content/public/browser/notification_source.h" 42 #include "grit/theme_resources.h" 43 #include "grit/ui_resources.h" 44 #include "ui/base/accelerators/platform_accelerator_gtk.h" 45 #include "ui/base/gtk/gtk_compat.h" 46 #include "ui/base/resource/resource_bundle.h" 47 #include "ui/gfx/canvas_skia_paint.h" 48 #include "ui/gfx/gtk_util.h" 49 #include "ui/gfx/image/image.h" 50 #include "ui/gfx/image/image_skia_operations.h" 51 52 using extensions::Extension; 53 using extensions::ExtensionActionManager; 54 55 namespace { 56 57 // The width of the browser action buttons. 58 const int kButtonWidth = 27; 59 60 // The padding between browser action buttons. 61 const int kButtonPadding = 4; 62 63 // The padding to the right of the browser action buttons (between the buttons 64 // and chevron if they are both showing). 65 const int kButtonChevronPadding = 2; 66 67 // The padding to the left, top and bottom of the browser actions toolbar 68 // separator. 69 const int kSeparatorPadding = 2; 70 71 // Width of the invisible gripper for resizing the toolbar. 72 const int kResizeGripperWidth = 4; 73 74 const char kDragTarget[] = "application/x-chrome-browseraction"; 75 76 GtkTargetEntry GetDragTargetEntry() { 77 GtkTargetEntry drag_target; 78 drag_target.target = const_cast<char*>(kDragTarget); 79 drag_target.flags = GTK_TARGET_SAME_APP; 80 drag_target.info = 0; 81 return drag_target; 82 } 83 84 // The minimum width in pixels of the button hbox if |icon_count| icons are 85 // showing. 86 gint WidthForIconCount(gint icon_count) { 87 return std::max((kButtonWidth + kButtonPadding) * icon_count - kButtonPadding, 88 0); 89 } 90 91 } // namespace 92 93 using ui::SimpleMenuModel; 94 95 class BrowserActionButton : public content::NotificationObserver, 96 public ExtensionActionIconFactory::Observer, 97 public ExtensionContextMenuModel::PopupDelegate, 98 public MenuGtk::Delegate { 99 public: 100 BrowserActionButton(BrowserActionsToolbarGtk* toolbar, 101 const Extension* extension, 102 GtkThemeService* theme_provider) 103 : toolbar_(toolbar), 104 extension_(extension), 105 image_(NULL), 106 icon_factory_(toolbar->browser()->profile(), extension, 107 browser_action(), this), 108 accel_group_(NULL) { 109 button_.reset(new CustomDrawButton( 110 theme_provider, 111 IDR_BROWSER_ACTION, 112 IDR_BROWSER_ACTION_P, 113 IDR_BROWSER_ACTION_H, 114 0, 115 NULL)); 116 gtk_widget_set_size_request(button(), kButtonWidth, kButtonWidth); 117 alignment_.Own(gtk_alignment_new(0, 0, 1, 1)); 118 gtk_container_add(GTK_CONTAINER(alignment_.get()), button()); 119 gtk_widget_show(button()); 120 121 DCHECK(browser_action()); 122 123 UpdateState(); 124 125 signals_.Connect(button(), "button-press-event", 126 G_CALLBACK(OnButtonPress), this); 127 signals_.Connect(button(), "clicked", 128 G_CALLBACK(OnClicked), this); 129 signals_.Connect(button(), "drag-begin", 130 G_CALLBACK(OnDragBegin), this); 131 signals_.ConnectAfter(widget(), "expose-event", 132 G_CALLBACK(OnExposeEvent), this); 133 if (toolbar_->browser()->window()) { 134 // If the window exists already, then the browser action button has been 135 // recreated after the window was created, for example when the extension 136 // is reloaded. 137 ConnectBrowserActionPopupAccelerator(); 138 } else { 139 // Window doesn't exist yet, wait for it. 140 signals_.Connect(toolbar->widget(), "realize", 141 G_CALLBACK(OnRealize), this); 142 } 143 144 registrar_.Add( 145 this, chrome::NOTIFICATION_EXTENSION_BROWSER_ACTION_UPDATED, 146 content::Source<ExtensionAction>(browser_action())); 147 registrar_.Add( 148 this, chrome::NOTIFICATION_EXTENSION_UNLOADED, 149 content::Source<Profile>( 150 toolbar->browser()->profile()->GetOriginalProfile())); 151 registrar_.Add( 152 this, chrome::NOTIFICATION_EXTENSION_COMMAND_ADDED, 153 content::Source<Profile>( 154 toolbar->browser()->profile()->GetOriginalProfile())); 155 registrar_.Add( 156 this, chrome::NOTIFICATION_EXTENSION_COMMAND_REMOVED, 157 content::Source<Profile>( 158 toolbar->browser()->profile()->GetOriginalProfile())); 159 } 160 161 virtual ~BrowserActionButton() { 162 DisconnectBrowserActionPopupAccelerator(); 163 164 alignment_.Destroy(); 165 } 166 167 GtkWidget* button() { return button_->widget(); } 168 169 GtkWidget* widget() { return alignment_.get(); } 170 171 const Extension* extension() { return extension_; } 172 173 // NotificationObserver implementation. 174 virtual void Observe(int type, 175 const content::NotificationSource& source, 176 const content::NotificationDetails& details) OVERRIDE { 177 switch (type) { 178 case chrome::NOTIFICATION_EXTENSION_BROWSER_ACTION_UPDATED: 179 UpdateState(); 180 break; 181 case chrome::NOTIFICATION_EXTENSION_UNLOADED: 182 case chrome::NOTIFICATION_WINDOW_CLOSED: 183 DisconnectBrowserActionPopupAccelerator(); 184 break; 185 case chrome::NOTIFICATION_EXTENSION_COMMAND_ADDED: 186 case chrome::NOTIFICATION_EXTENSION_COMMAND_REMOVED: { 187 std::pair<const std::string, const std::string>* payload = 188 content::Details<std::pair<const std::string, const std::string> >( 189 details).ptr(); 190 if (extension_->id() == payload->first && 191 payload->second == 192 extension_manifest_values::kBrowserActionCommandEvent) { 193 if (type == chrome::NOTIFICATION_EXTENSION_COMMAND_ADDED) 194 ConnectBrowserActionPopupAccelerator(); 195 else 196 DisconnectBrowserActionPopupAccelerator(); 197 } 198 break; 199 } 200 default: 201 NOTREACHED(); 202 break; 203 } 204 } 205 206 // ExtensionActionIconFactory::Observer implementation. 207 virtual void OnIconUpdated() OVERRIDE { 208 UpdateState(); 209 } 210 211 // Updates the button based on the latest state from the associated 212 // browser action. 213 void UpdateState() { 214 int tab_id = toolbar_->GetCurrentTabId(); 215 if (tab_id < 0) 216 return; 217 218 std::string tooltip = browser_action()->GetTitle(tab_id); 219 if (tooltip.empty()) 220 gtk_widget_set_has_tooltip(button(), FALSE); 221 else 222 gtk_widget_set_tooltip_text(button(), tooltip.c_str()); 223 224 enabled_ = browser_action()->GetIsVisible(tab_id); 225 if (!enabled_) 226 button_->SetPaintOverride(GTK_STATE_INSENSITIVE); 227 else 228 button_->UnsetPaintOverride(); 229 230 gfx::Image image = icon_factory_.GetIcon(tab_id); 231 if (!image.IsEmpty()) { 232 if (enabled_) { 233 SetImage(image); 234 } else { 235 SetImage(gfx::Image(gfx::ImageSkiaOperations::CreateTransparentImage( 236 image.AsImageSkia(), .25))); 237 } 238 } 239 240 gtk_widget_queue_draw(button()); 241 } 242 243 gfx::Image GetIcon() { 244 return icon_factory_.GetIcon(toolbar_->GetCurrentTabId()); 245 } 246 247 MenuGtk* GetContextMenu() { 248 if (!extension_->ShowConfigureContextMenus()) 249 return NULL; 250 251 context_menu_model_ = 252 new ExtensionContextMenuModel(extension_, toolbar_->browser(), this); 253 context_menu_.reset( 254 new MenuGtk(this, context_menu_model_.get())); 255 return context_menu_.get(); 256 } 257 258 private: 259 // Activate the browser action. 260 void Activate(GtkWidget* widget) { 261 ExtensionToolbarModel* model = toolbar_->model(); 262 const Extension* extension = extension_; 263 Browser* browser = toolbar_->browser(); 264 GURL popup_url; 265 266 switch (model->ExecuteBrowserAction(extension, browser, &popup_url)) { 267 case ExtensionToolbarModel::ACTION_NONE: 268 break; 269 case ExtensionToolbarModel::ACTION_SHOW_POPUP: 270 ExtensionPopupGtk::Show(popup_url, browser, widget, 271 ExtensionPopupGtk::SHOW); 272 break; 273 } 274 } 275 276 // MenuGtk::Delegate implementation. 277 virtual void StoppedShowing() OVERRIDE { 278 if (enabled_) 279 button_->UnsetPaintOverride(); 280 else 281 button_->SetPaintOverride(GTK_STATE_INSENSITIVE); 282 283 // If the context menu was showing for the overflow menu, re-assert the 284 // grab that was shadowed. 285 if (toolbar_->overflow_menu_.get()) 286 gtk_util::GrabAllInput(toolbar_->overflow_menu_->widget()); 287 } 288 289 virtual void CommandWillBeExecuted() OVERRIDE { 290 // If the context menu was showing for the overflow menu, and a command 291 // is executed, then stop showing the overflow menu. 292 if (toolbar_->overflow_menu_.get()) 293 toolbar_->overflow_menu_->Cancel(); 294 } 295 296 // ExtensionContextMenuModel::PopupDelegate implementation. 297 virtual void InspectPopup(ExtensionAction* action) OVERRIDE { 298 GURL popup_url = action->GetPopupUrl(toolbar_->GetCurrentTabId()); 299 ExtensionPopupGtk::Show(popup_url, toolbar_->browser(), widget(), 300 ExtensionPopupGtk::SHOW_AND_INSPECT); 301 } 302 303 void SetImage(const gfx::Image& image) { 304 if (!image_) { 305 image_ = gtk_image_new_from_pixbuf(image.ToGdkPixbuf()); 306 gtk_button_set_image(GTK_BUTTON(button()), image_); 307 } else { 308 gtk_image_set_from_pixbuf(GTK_IMAGE(image_), image.ToGdkPixbuf()); 309 } 310 } 311 312 static gboolean OnButtonPress(GtkWidget* widget, 313 GdkEventButton* event, 314 BrowserActionButton* button) { 315 if (event->button != 3) 316 return FALSE; 317 318 MenuGtk* menu = button->GetContextMenu(); 319 if (!menu) 320 return FALSE; 321 322 button->button_->SetPaintOverride(GTK_STATE_ACTIVE); 323 menu->PopupForWidget(widget, event->button, event->time); 324 325 return TRUE; 326 } 327 328 static void OnClicked(GtkWidget* widget, BrowserActionButton* button) { 329 if (button->enabled_) 330 button->Activate(widget); 331 } 332 333 static gboolean OnExposeEvent(GtkWidget* widget, 334 GdkEventExpose* event, 335 BrowserActionButton* button) { 336 int tab_id = button->toolbar_->GetCurrentTabId(); 337 if (tab_id < 0) 338 return FALSE; 339 340 ExtensionAction* action = button->browser_action(); 341 if (action->GetBadgeText(tab_id).empty()) 342 return FALSE; 343 344 gfx::CanvasSkiaPaint canvas(event, false); 345 GtkAllocation allocation; 346 gtk_widget_get_allocation(widget, &allocation); 347 action->PaintBadge(&canvas, gfx::Rect(allocation), tab_id); 348 return FALSE; 349 } 350 351 static void OnDragBegin(GtkWidget* widget, 352 GdkDragContext* drag_context, 353 BrowserActionButton* button) { 354 // Simply pass along the notification to the toolbar. The point of this 355 // function is to tell the toolbar which BrowserActionButton initiated the 356 // drag. 357 button->toolbar_->DragStarted(button, drag_context); 358 } 359 360 // The accelerator handler for when the shortcuts to open the popup is struck. 361 static gboolean OnGtkAccelerator(GtkAccelGroup* accel_group, 362 GObject* acceleratable, 363 guint keyval, 364 GdkModifierType modifier, 365 BrowserActionButton* button) { 366 // Open the popup for this extension. 367 GtkWidget* anchor = button->widget(); 368 // The anchor might be in the overflow menu. Then we point to the chevron. 369 if (!gtk_widget_get_visible(anchor)) 370 anchor = button->toolbar_->chevron(); 371 button->Activate(anchor); 372 return TRUE; 373 } 374 375 // The handler for when the browser action is realized. |user_data| contains a 376 // pointer to the BrowserAction shown. 377 static void OnRealize(GtkWidget* widget, void* user_data) { 378 BrowserActionButton* button = static_cast<BrowserActionButton*>(user_data); 379 button->ConnectBrowserActionPopupAccelerator(); 380 } 381 382 // Connect the accelerator for the browser action popup. 383 void ConnectBrowserActionPopupAccelerator() { 384 extensions::CommandService* command_service = 385 extensions::CommandService::Get(toolbar_->browser()->profile()); 386 extensions::Command command; 387 if (command_service->GetBrowserActionCommand(extension_->id(), 388 extensions::CommandService::ACTIVE_ONLY, 389 &command, 390 NULL)) { 391 // Found the browser action shortcut command, register it. 392 keybinding_ = command.accelerator(); 393 394 gfx::NativeWindow window = 395 toolbar_->browser()->window()->GetNativeWindow(); 396 accel_group_ = gtk_accel_group_new(); 397 gtk_window_add_accel_group(window, accel_group_); 398 399 gtk_accel_group_connect( 400 accel_group_, 401 ui::GetGdkKeyCodeForAccelerator(keybinding_), 402 ui::GetGdkModifierForAccelerator(keybinding_), 403 GtkAccelFlags(0), 404 g_cclosure_new(G_CALLBACK(OnGtkAccelerator), this, NULL)); 405 406 // Since we've added an accelerator, we'll need to unregister it before 407 // the window is closed, so we listen for the window being closed. 408 registrar_.Add(this, 409 chrome::NOTIFICATION_WINDOW_CLOSED, 410 content::Source<GtkWindow>(window)); 411 } 412 } 413 414 // Disconnect the accelerator for the browser action popup and delete clean up 415 // the accelerator group registration. 416 void DisconnectBrowserActionPopupAccelerator() { 417 if (accel_group_) { 418 gfx::NativeWindow window = 419 toolbar_->browser()->window()->GetNativeWindow(); 420 gtk_accel_group_disconnect_key( 421 accel_group_, 422 ui::GetGdkKeyCodeForAccelerator(keybinding_), 423 GetGdkModifierForAccelerator(keybinding_)); 424 gtk_window_remove_accel_group(window, accel_group_); 425 g_object_unref(accel_group_); 426 accel_group_ = NULL; 427 keybinding_ = ui::Accelerator(); 428 429 // We've removed the accelerator, so no need to listen to this anymore. 430 registrar_.Remove(this, 431 chrome::NOTIFICATION_WINDOW_CLOSED, 432 content::Source<GtkWindow>(window)); 433 } 434 } 435 436 ExtensionAction* browser_action() const { 437 return ExtensionActionManager::Get(toolbar_->browser()->profile())-> 438 GetBrowserAction(*extension_); 439 } 440 441 // The toolbar containing this button. 442 BrowserActionsToolbarGtk* toolbar_; 443 444 // The extension that contains this browser action. 445 const Extension* extension_; 446 447 // The button for this browser action. 448 scoped_ptr<CustomDrawButton> button_; 449 450 // Whether the browser action is enabled (equivalent to whether a page action 451 // is visible). 452 bool enabled_; 453 454 // The top level widget (parent of |button_|). 455 ui::OwnedWidgetGtk alignment_; 456 457 // The one image subwidget in |button_|. We keep this out so we don't alter 458 // the widget hierarchy while changing the button image because changing the 459 // GTK widget hierarchy invalidates all tooltips and several popular 460 // extensions change browser action icon in a loop. 461 GtkWidget* image_; 462 463 // The object that will be used to get the browser action icon for us. 464 // It may load the icon asynchronously (in which case the initial icon 465 // returned by the factory will be transparent), so we have to observe it for 466 // updates to the icon. 467 ExtensionActionIconFactory icon_factory_; 468 469 // Same as |default_icon_|, but stored as SkBitmap. 470 SkBitmap default_skbitmap_; 471 472 ui::GtkSignalRegistrar signals_; 473 content::NotificationRegistrar registrar_; 474 475 // The accelerator group used to handle accelerators, owned by this object. 476 GtkAccelGroup* accel_group_; 477 478 // The keybinding accelerator registered to show the browser action popup. 479 ui::Accelerator keybinding_; 480 481 // The context menu view and model for this extension action. 482 scoped_ptr<MenuGtk> context_menu_; 483 scoped_refptr<ExtensionContextMenuModel> context_menu_model_; 484 485 friend class BrowserActionsToolbarGtk; 486 }; 487 488 // BrowserActionsToolbarGtk ---------------------------------------------------- 489 490 BrowserActionsToolbarGtk::BrowserActionsToolbarGtk(Browser* browser) 491 : browser_(browser), 492 profile_(browser->profile()), 493 theme_service_(GtkThemeService::GetFrom(browser->profile())), 494 model_(NULL), 495 hbox_(gtk_hbox_new(FALSE, 0)), 496 button_hbox_(gtk_chrome_shrinkable_hbox_new(TRUE, FALSE, kButtonPadding)), 497 drag_button_(NULL), 498 drop_index_(-1), 499 resize_animation_(this), 500 desired_width_(0), 501 start_width_(0), 502 weak_factory_(this) { 503 ExtensionService* extension_service = 504 extensions::ExtensionSystem::Get(profile_)->extension_service(); 505 if (!extension_service) 506 return; 507 508 overflow_button_.reset(new CustomDrawButton( 509 theme_service_, 510 IDR_BROWSER_ACTIONS_OVERFLOW, 511 IDR_BROWSER_ACTIONS_OVERFLOW_P, 512 IDR_BROWSER_ACTIONS_OVERFLOW_H, 513 0, 514 gtk_arrow_new(GTK_ARROW_DOWN, GTK_SHADOW_NONE))); 515 516 GtkWidget* gripper = gtk_button_new(); 517 gtk_widget_set_size_request(gripper, kResizeGripperWidth, -1); 518 gtk_widget_set_can_focus(gripper, FALSE); 519 520 gtk_widget_add_events(gripper, GDK_POINTER_MOTION_MASK); 521 signals_.Connect(gripper, "motion-notify-event", 522 G_CALLBACK(OnGripperMotionNotifyThunk), this); 523 signals_.Connect(gripper, "expose-event", 524 G_CALLBACK(OnGripperExposeThunk), this); 525 signals_.Connect(gripper, "enter-notify-event", 526 G_CALLBACK(OnGripperEnterNotifyThunk), this); 527 signals_.Connect(gripper, "leave-notify-event", 528 G_CALLBACK(OnGripperLeaveNotifyThunk), this); 529 signals_.Connect(gripper, "button-release-event", 530 G_CALLBACK(OnGripperButtonReleaseThunk), this); 531 signals_.Connect(gripper, "button-press-event", 532 G_CALLBACK(OnGripperButtonPressThunk), this); 533 signals_.Connect(chevron(), "button-press-event", 534 G_CALLBACK(OnOverflowButtonPressThunk), this); 535 536 // |overflow_alignment| adds padding to the right of the browser action 537 // buttons, but only appears when the overflow menu is showing. 538 overflow_alignment_.Own(gtk_alignment_new(0, 0, 1, 1)); 539 gtk_container_add(GTK_CONTAINER(overflow_alignment_.get()), chevron()); 540 541 // |overflow_area_| holds the overflow chevron and the separator, which 542 // is only shown in GTK+ theme mode. 543 overflow_area_.Own(gtk_hbox_new(FALSE, 0)); 544 gtk_box_pack_start(GTK_BOX(overflow_area_.get()), overflow_alignment_.get(), 545 FALSE, FALSE, 0); 546 547 separator_.Own(gtk_vseparator_new()); 548 gtk_box_pack_start(GTK_BOX(overflow_area_.get()), separator_.get(), 549 FALSE, FALSE, 0); 550 gtk_widget_set_no_show_all(separator_.get(), TRUE); 551 552 gtk_widget_show_all(overflow_area_.get()); 553 gtk_widget_set_no_show_all(overflow_area_.get(), TRUE); 554 555 gtk_box_pack_start(GTK_BOX(hbox_.get()), gripper, FALSE, FALSE, 0); 556 gtk_box_pack_start(GTK_BOX(hbox_.get()), button_hbox_.get(), TRUE, TRUE, 0); 557 gtk_box_pack_start(GTK_BOX(hbox_.get()), overflow_area_.get(), FALSE, FALSE, 558 0); 559 560 model_ = extension_service->toolbar_model(); 561 model_->AddObserver(this); 562 SetupDrags(); 563 564 if (model_->extensions_initialized()) { 565 CreateAllButtons(); 566 SetContainerWidth(); 567 } 568 569 // We want to connect to "set-focus" on the toplevel window; we have to wait 570 // until we are added to a toplevel window to do so. 571 signals_.Connect(widget(), "hierarchy-changed", 572 G_CALLBACK(OnHierarchyChangedThunk), this); 573 574 ViewIDUtil::SetID(button_hbox_.get(), VIEW_ID_BROWSER_ACTION_TOOLBAR); 575 576 registrar_.Add(this, 577 chrome::NOTIFICATION_BROWSER_THEME_CHANGED, 578 content::Source<ThemeService>(theme_service_)); 579 theme_service_->InitThemesFor(this); 580 } 581 582 BrowserActionsToolbarGtk::~BrowserActionsToolbarGtk() { 583 if (model_) 584 model_->RemoveObserver(this); 585 button_hbox_.Destroy(); 586 hbox_.Destroy(); 587 } 588 589 int BrowserActionsToolbarGtk::GetCurrentTabId() const { 590 content::WebContents* active_tab = 591 browser_->tab_strip_model()->GetActiveWebContents(); 592 if (!active_tab) 593 return -1; 594 595 return SessionTabHelper::FromWebContents(active_tab)->session_id().id(); 596 } 597 598 void BrowserActionsToolbarGtk::Update() { 599 for (ExtensionButtonMap::iterator iter = extension_button_map_.begin(); 600 iter != extension_button_map_.end(); ++iter) { 601 iter->second->UpdateState(); 602 } 603 } 604 605 void BrowserActionsToolbarGtk::Observe( 606 int type, 607 const content::NotificationSource& source, 608 const content::NotificationDetails& details) { 609 DCHECK(chrome::NOTIFICATION_BROWSER_THEME_CHANGED == type); 610 gtk_widget_set_visible(separator_.get(), theme_service_->UsingNativeTheme()); 611 } 612 613 void BrowserActionsToolbarGtk::SetupDrags() { 614 GtkTargetEntry drag_target = GetDragTargetEntry(); 615 gtk_drag_dest_set(button_hbox_.get(), GTK_DEST_DEFAULT_DROP, &drag_target, 1, 616 GDK_ACTION_MOVE); 617 618 signals_.Connect(button_hbox_.get(), "drag-motion", 619 G_CALLBACK(OnDragMotionThunk), this); 620 } 621 622 void BrowserActionsToolbarGtk::CreateAllButtons() { 623 extension_button_map_.clear(); 624 625 int i = 0; 626 const extensions::ExtensionList& toolbar_items = model_->toolbar_items(); 627 for (extensions::ExtensionList::const_iterator iter = toolbar_items.begin(); 628 iter != toolbar_items.end(); ++iter) { 629 CreateButtonForExtension(iter->get(), i++); 630 } 631 } 632 633 void BrowserActionsToolbarGtk::SetContainerWidth() { 634 int showing_actions = model_->GetVisibleIconCount(); 635 if (showing_actions >= 0) 636 SetButtonHBoxWidth(WidthForIconCount(showing_actions)); 637 } 638 639 void BrowserActionsToolbarGtk::CreateButtonForExtension( 640 const Extension* extension, int index) { 641 if (!ShouldDisplayBrowserAction(extension)) 642 return; 643 644 if (profile_->IsOffTheRecord()) 645 index = model_->OriginalIndexToIncognito(index); 646 647 RemoveButtonForExtension(extension); 648 linked_ptr<BrowserActionButton> button( 649 new BrowserActionButton(this, extension, theme_service_)); 650 gtk_chrome_shrinkable_hbox_pack_start( 651 GTK_CHROME_SHRINKABLE_HBOX(button_hbox_.get()), button->widget(), 0); 652 gtk_box_reorder_child(GTK_BOX(button_hbox_.get()), button->widget(), index); 653 extension_button_map_[extension->id()] = button; 654 655 GtkTargetEntry drag_target = GetDragTargetEntry(); 656 gtk_drag_source_set(button->button(), GDK_BUTTON1_MASK, &drag_target, 1, 657 GDK_ACTION_MOVE); 658 // We ignore whether the drag was a "success" or "failure" in Gtk's opinion. 659 signals_.Connect(button->button(), "drag-end", 660 G_CALLBACK(&OnDragEndThunk), this); 661 signals_.Connect(button->button(), "drag-failed", 662 G_CALLBACK(&OnDragFailedThunk), this); 663 664 // Any time a browser action button is shown or hidden we have to update 665 // the chevron state. 666 signals_.Connect(button->widget(), "show", 667 G_CALLBACK(&OnButtonShowOrHideThunk), this); 668 signals_.Connect(button->widget(), "hide", 669 G_CALLBACK(&OnButtonShowOrHideThunk), this); 670 671 gtk_widget_show(button->widget()); 672 673 UpdateVisibility(); 674 } 675 676 GtkWidget* BrowserActionsToolbarGtk::GetBrowserActionWidget( 677 const Extension* extension) { 678 ExtensionButtonMap::iterator it = extension_button_map_.find( 679 extension->id()); 680 if (it == extension_button_map_.end()) 681 return NULL; 682 683 return it->second.get()->widget(); 684 } 685 686 void BrowserActionsToolbarGtk::RemoveButtonForExtension( 687 const Extension* extension) { 688 if (extension_button_map_.erase(extension->id())) 689 UpdateVisibility(); 690 UpdateChevronVisibility(); 691 } 692 693 void BrowserActionsToolbarGtk::UpdateVisibility() { 694 gtk_widget_set_visible(widget(), button_count() != 0); 695 } 696 697 bool BrowserActionsToolbarGtk::ShouldDisplayBrowserAction( 698 const Extension* extension) { 699 // Only display incognito-enabled extensions while in incognito mode. 700 return (!profile_->IsOffTheRecord() || 701 extensions::ExtensionSystem::Get(profile_)->extension_service()-> 702 IsIncognitoEnabled(extension->id())); 703 } 704 705 void BrowserActionsToolbarGtk::HidePopup() { 706 ExtensionPopupGtk* popup = ExtensionPopupGtk::get_current_extension_popup(); 707 if (popup) 708 popup->DestroyPopup(); 709 } 710 711 void BrowserActionsToolbarGtk::AnimateToShowNIcons(int count) { 712 desired_width_ = WidthForIconCount(count); 713 714 GtkAllocation allocation; 715 gtk_widget_get_allocation(button_hbox_.get(), &allocation); 716 start_width_ = allocation.width; 717 718 resize_animation_.Reset(); 719 resize_animation_.Show(); 720 } 721 722 void BrowserActionsToolbarGtk::BrowserActionAdded(const Extension* extension, 723 int index) { 724 overflow_menu_.reset(); 725 726 CreateButtonForExtension(extension, index); 727 728 // If we are still initializing the container, don't bother animating. 729 if (!model_->extensions_initialized()) 730 return; 731 732 // Animate the addition if we are showing all browser action buttons. 733 if (!gtk_widget_get_visible(overflow_area_.get())) { 734 AnimateToShowNIcons(button_count()); 735 model_->SetVisibleIconCount(button_count()); 736 } 737 } 738 739 void BrowserActionsToolbarGtk::BrowserActionRemoved( 740 const Extension* extension) { 741 overflow_menu_.reset(); 742 743 if (drag_button_ != NULL) { 744 // Break the current drag. 745 gtk_grab_remove(button_hbox_.get()); 746 } 747 748 RemoveButtonForExtension(extension); 749 750 if (!gtk_widget_get_visible(overflow_area_.get())) { 751 AnimateToShowNIcons(button_count()); 752 model_->SetVisibleIconCount(button_count()); 753 } 754 } 755 756 void BrowserActionsToolbarGtk::BrowserActionMoved(const Extension* extension, 757 int index) { 758 // We initiated this move action, and have already moved the button. 759 if (drag_button_ != NULL) 760 return; 761 762 GtkWidget* button_widget = GetBrowserActionWidget(extension); 763 if (!button_widget) { 764 if (ShouldDisplayBrowserAction(extension)) 765 NOTREACHED(); 766 return; 767 } 768 769 if (profile_->IsOffTheRecord()) 770 index = model_->OriginalIndexToIncognito(index); 771 772 gtk_box_reorder_child(GTK_BOX(button_hbox_.get()), button_widget, index); 773 } 774 775 void BrowserActionsToolbarGtk::ModelLoaded() { 776 SetContainerWidth(); 777 } 778 779 void BrowserActionsToolbarGtk::AnimationProgressed( 780 const ui::Animation* animation) { 781 int width = start_width_ + (desired_width_ - start_width_) * 782 animation->GetCurrentValue(); 783 gtk_widget_set_size_request(button_hbox_.get(), width, -1); 784 785 if (width == desired_width_) 786 resize_animation_.Reset(); 787 } 788 789 void BrowserActionsToolbarGtk::AnimationEnded(const ui::Animation* animation) { 790 gtk_widget_set_size_request(button_hbox_.get(), desired_width_, -1); 791 UpdateChevronVisibility(); 792 } 793 794 bool BrowserActionsToolbarGtk::IsCommandIdChecked(int command_id) const { 795 return false; 796 } 797 798 bool BrowserActionsToolbarGtk::IsCommandIdEnabled(int command_id) const { 799 const Extension* extension = model_->toolbar_items()[command_id].get(); 800 return ExtensionActionManager::Get(profile_)->GetBrowserAction(*extension) 801 ->GetIsVisible(GetCurrentTabId()); 802 } 803 804 bool BrowserActionsToolbarGtk::GetAcceleratorForCommandId( 805 int command_id, 806 ui::Accelerator* accelerator) { 807 return false; 808 } 809 810 void BrowserActionsToolbarGtk::ExecuteCommand(int command_id, int event_flags) { 811 const Extension* extension = model_->toolbar_items()[command_id].get(); 812 GURL popup_url; 813 814 switch (model_->ExecuteBrowserAction(extension, browser(), &popup_url)) { 815 case ExtensionToolbarModel::ACTION_NONE: 816 break; 817 case ExtensionToolbarModel::ACTION_SHOW_POPUP: 818 ExtensionPopupGtk::Show(popup_url, browser(), chevron(), 819 ExtensionPopupGtk::SHOW); 820 break; 821 } 822 } 823 824 void BrowserActionsToolbarGtk::StoppedShowing() { 825 overflow_button_->UnsetPaintOverride(); 826 } 827 828 bool BrowserActionsToolbarGtk::AlwaysShowIconForCmd(int command_id) const { 829 return true; 830 } 831 832 void BrowserActionsToolbarGtk::DragStarted(BrowserActionButton* button, 833 GdkDragContext* drag_context) { 834 // No representation of the widget following the cursor. 835 GdkPixbuf* pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, TRUE, 8, 1, 1); 836 gtk_drag_set_icon_pixbuf(drag_context, pixbuf, 0, 0); 837 g_object_unref(pixbuf); 838 839 DCHECK(!drag_button_); 840 drag_button_ = button; 841 } 842 843 void BrowserActionsToolbarGtk::SetButtonHBoxWidth(int new_width) { 844 gint max_width = WidthForIconCount(button_count()); 845 new_width = std::min(max_width, new_width); 846 new_width = std::max(new_width, 0); 847 gtk_widget_set_size_request(button_hbox_.get(), new_width, -1); 848 } 849 850 void BrowserActionsToolbarGtk::UpdateChevronVisibility() { 851 int showing_icon_count = 852 gtk_chrome_shrinkable_hbox_get_visible_child_count( 853 GTK_CHROME_SHRINKABLE_HBOX(button_hbox_.get())); 854 if (showing_icon_count == 0) { 855 gtk_alignment_set_padding(GTK_ALIGNMENT(overflow_alignment_.get()), 856 0, 0, 0, 0); 857 } else { 858 gtk_alignment_set_padding(GTK_ALIGNMENT(overflow_alignment_.get()), 859 0, 0, kButtonChevronPadding, 0); 860 } 861 862 if (button_count() > showing_icon_count) { 863 if (!gtk_widget_get_visible(overflow_area_.get())) { 864 if (drag_button_) { 865 // During drags, when the overflow chevron shows for the first time, 866 // take that much space away from |button_hbox_| to make the drag look 867 // smoother. 868 GtkRequisition req; 869 gtk_widget_size_request(chevron(), &req); 870 gint overflow_width = req.width; 871 gtk_widget_size_request(button_hbox_.get(), &req); 872 gint button_hbox_width = req.width; 873 button_hbox_width = std::max(button_hbox_width - overflow_width, 0); 874 gtk_widget_set_size_request(button_hbox_.get(), button_hbox_width, -1); 875 } 876 877 gtk_widget_show(overflow_area_.get()); 878 } 879 } else { 880 gtk_widget_hide(overflow_area_.get()); 881 } 882 } 883 884 gboolean BrowserActionsToolbarGtk::OnDragMotion(GtkWidget* widget, 885 GdkDragContext* drag_context, 886 gint x, gint y, guint time) { 887 // Only handle drags we initiated. 888 if (!drag_button_) 889 return FALSE; 890 891 if (base::i18n::IsRTL()) { 892 GtkAllocation allocation; 893 gtk_widget_get_allocation(widget, &allocation); 894 x = allocation.width - x; 895 } 896 897 drop_index_ = x < kButtonWidth ? 0 : x / (kButtonWidth + kButtonPadding); 898 899 // We will go ahead and reorder the child in order to provide visual feedback 900 // to the user. We don't inform the model that it has moved until the drag 901 // ends. 902 gtk_box_reorder_child(GTK_BOX(button_hbox_.get()), drag_button_->widget(), 903 drop_index_); 904 905 gdk_drag_status(drag_context, GDK_ACTION_MOVE, time); 906 return TRUE; 907 } 908 909 void BrowserActionsToolbarGtk::OnDragEnd(GtkWidget* button, 910 GdkDragContext* drag_context) { 911 if (drop_index_ != -1) { 912 if (profile_->IsOffTheRecord()) 913 drop_index_ = model_->IncognitoIndexToOriginal(drop_index_); 914 915 model_->MoveBrowserAction(drag_button_->extension(), drop_index_); 916 } 917 918 drag_button_ = NULL; 919 drop_index_ = -1; 920 } 921 922 gboolean BrowserActionsToolbarGtk::OnDragFailed(GtkWidget* widget, 923 GdkDragContext* drag_context, 924 GtkDragResult result) { 925 // We connect to this signal and return TRUE so that the default failure 926 // animation (wherein the drag widget floats back to the start of the drag) 927 // does not show, and the drag-end signal is emitted immediately instead of 928 // several seconds later. 929 return TRUE; 930 } 931 932 void BrowserActionsToolbarGtk::OnHierarchyChanged( 933 GtkWidget* widget, GtkWidget* previous_toplevel) { 934 GtkWidget* toplevel = gtk_widget_get_toplevel(widget); 935 if (!gtk_widget_is_toplevel(toplevel)) 936 return; 937 938 signals_.Connect(toplevel, "set-focus", G_CALLBACK(OnSetFocusThunk), this); 939 } 940 941 void BrowserActionsToolbarGtk::OnSetFocus(GtkWidget* widget, 942 GtkWidget* focus_widget) { 943 ExtensionPopupGtk* popup = ExtensionPopupGtk::get_current_extension_popup(); 944 // The focus of the parent window has changed. Close the popup. Delay the hide 945 // because it will destroy the RenderViewHost, which may still be on the 946 // call stack. 947 if (!popup || popup->being_inspected()) 948 return; 949 base::MessageLoop::current()->PostTask( 950 FROM_HERE, 951 base::Bind(&BrowserActionsToolbarGtk::HidePopup, 952 weak_factory_.GetWeakPtr())); 953 } 954 955 gboolean BrowserActionsToolbarGtk::OnGripperMotionNotify( 956 GtkWidget* widget, GdkEventMotion* event) { 957 if (!(event->state & GDK_BUTTON1_MASK)) 958 return FALSE; 959 960 // Calculate how much the user dragged the gripper and subtract that off the 961 // button container's width. 962 int distance_dragged; 963 if (base::i18n::IsRTL()) { 964 distance_dragged = -event->x; 965 } else { 966 GtkAllocation widget_allocation; 967 gtk_widget_get_allocation(widget, &widget_allocation); 968 distance_dragged = event->x - widget_allocation.width; 969 } 970 971 GtkAllocation button_hbox_allocation; 972 gtk_widget_get_allocation(button_hbox_.get(), &button_hbox_allocation); 973 gint new_width = button_hbox_allocation.width - distance_dragged; 974 SetButtonHBoxWidth(new_width); 975 976 return FALSE; 977 } 978 979 gboolean BrowserActionsToolbarGtk::OnGripperExpose(GtkWidget* gripper, 980 GdkEventExpose* expose) { 981 return TRUE; 982 } 983 984 // These three signal handlers (EnterNotify, LeaveNotify, and ButtonRelease) 985 // are used to give the gripper the resize cursor. Since it doesn't have its 986 // own window, we have to set the cursor whenever the pointer moves into the 987 // button or leaves the button, and be sure to leave it on when the user is 988 // dragging. 989 gboolean BrowserActionsToolbarGtk::OnGripperEnterNotify( 990 GtkWidget* gripper, GdkEventCrossing* event) { 991 gdk_window_set_cursor(gtk_widget_get_window(gripper), 992 gfx::GetCursor(GDK_SB_H_DOUBLE_ARROW)); 993 return FALSE; 994 } 995 996 gboolean BrowserActionsToolbarGtk::OnGripperLeaveNotify( 997 GtkWidget* gripper, GdkEventCrossing* event) { 998 if (!(event->state & GDK_BUTTON1_MASK)) 999 gdk_window_set_cursor(gtk_widget_get_window(gripper), NULL); 1000 return FALSE; 1001 } 1002 1003 gboolean BrowserActionsToolbarGtk::OnGripperButtonRelease( 1004 GtkWidget* gripper, GdkEventButton* event) { 1005 GtkAllocation allocation; 1006 gtk_widget_get_allocation(gripper, &allocation); 1007 gfx::Rect gripper_rect(0, 0, allocation.width, allocation.height); 1008 1009 gfx::Point release_point(event->x, event->y); 1010 if (!gripper_rect.Contains(release_point)) 1011 gdk_window_set_cursor(gtk_widget_get_window(gripper), NULL); 1012 1013 // After the user resizes the toolbar, we want to smartly resize it to be 1014 // the perfect size to fit the buttons. 1015 int visible_icon_count = 1016 gtk_chrome_shrinkable_hbox_get_visible_child_count( 1017 GTK_CHROME_SHRINKABLE_HBOX(button_hbox_.get())); 1018 AnimateToShowNIcons(visible_icon_count); 1019 model_->SetVisibleIconCount(visible_icon_count); 1020 1021 return FALSE; 1022 } 1023 1024 gboolean BrowserActionsToolbarGtk::OnGripperButtonPress( 1025 GtkWidget* gripper, GdkEventButton* event) { 1026 resize_animation_.Reset(); 1027 1028 return FALSE; 1029 } 1030 1031 gboolean BrowserActionsToolbarGtk::OnOverflowButtonPress( 1032 GtkWidget* overflow, GdkEventButton* event) { 1033 overflow_menu_model_.reset(new SimpleMenuModel(this)); 1034 1035 int visible_icon_count = 1036 gtk_chrome_shrinkable_hbox_get_visible_child_count( 1037 GTK_CHROME_SHRINKABLE_HBOX(button_hbox_.get())); 1038 for (int i = visible_icon_count; i < button_count(); ++i) { 1039 int model_index = i; 1040 if (profile_->IsOffTheRecord()) 1041 model_index = model_->IncognitoIndexToOriginal(i); 1042 1043 const Extension* extension = model_->toolbar_items()[model_index].get(); 1044 BrowserActionButton* button = extension_button_map_[extension->id()].get(); 1045 1046 overflow_menu_model_->AddItem(model_index, UTF8ToUTF16(extension->name())); 1047 overflow_menu_model_->SetIcon(overflow_menu_model_->GetItemCount() - 1, 1048 button->GetIcon()); 1049 1050 // TODO(estade): set the menu item's tooltip. 1051 } 1052 1053 overflow_menu_.reset(new MenuGtk(this, overflow_menu_model_.get())); 1054 signals_.Connect(overflow_menu_->widget(), "button-press-event", 1055 G_CALLBACK(OnOverflowMenuButtonPressThunk), this); 1056 1057 overflow_button_->SetPaintOverride(GTK_STATE_ACTIVE); 1058 overflow_menu_->PopupAsFromKeyEvent(chevron()); 1059 1060 return FALSE; 1061 } 1062 1063 gboolean BrowserActionsToolbarGtk::OnOverflowMenuButtonPress( 1064 GtkWidget* overflow, GdkEventButton* event) { 1065 if (event->button != 3) 1066 return FALSE; 1067 1068 GtkWidget* menu_item = GTK_MENU_SHELL(overflow)->active_menu_item; 1069 if (!menu_item) 1070 return FALSE; 1071 1072 int item_index = g_list_index(GTK_MENU_SHELL(overflow)->children, menu_item); 1073 if (item_index == -1) { 1074 NOTREACHED(); 1075 return FALSE; 1076 } 1077 1078 item_index += gtk_chrome_shrinkable_hbox_get_visible_child_count( 1079 GTK_CHROME_SHRINKABLE_HBOX(button_hbox_.get())); 1080 if (profile_->IsOffTheRecord()) 1081 item_index = model_->IncognitoIndexToOriginal(item_index); 1082 1083 const Extension* extension = model_->toolbar_items()[item_index].get(); 1084 ExtensionButtonMap::iterator it = extension_button_map_.find(extension->id()); 1085 if (it == extension_button_map_.end()) { 1086 NOTREACHED(); 1087 return FALSE; 1088 } 1089 1090 MenuGtk* menu = it->second.get()->GetContextMenu(); 1091 if (!menu) 1092 return FALSE; 1093 1094 menu->PopupAsContext(gfx::Point(event->x_root, event->y_root), 1095 event->time); 1096 return TRUE; 1097 } 1098 1099 void BrowserActionsToolbarGtk::OnButtonShowOrHide(GtkWidget* sender) { 1100 if (!resize_animation_.is_animating()) 1101 UpdateChevronVisibility(); 1102 } 1103