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