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 |