| OLD | NEW |
| (Empty) | |
| 1 // Copyright (c) 2009 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 #import "browser_actions_controller.h" |
| 6 |
| 7 #include <string> |
| 8 |
| 9 #include "app/gfx/canvas_paint.h" |
| 10 #include "base/sys_string_conversions.h" |
| 11 #include "chrome/browser/browser.h" |
| 12 #include "chrome/browser/cocoa/toolbar_button_cell.h" |
| 13 #include "chrome/browser/extensions/extension_browser_event_router.h" |
| 14 #include "chrome/browser/extensions/extensions_service.h" |
| 15 #include "chrome/browser/extensions/extension_tabs_module.h" |
| 16 #include "chrome/browser/extensions/image_loading_tracker.h" |
| 17 #include "chrome/browser/profile.h" |
| 18 #include "chrome/browser/tab_contents/tab_contents.h" |
| 19 #include "chrome/common/notification_observer.h" |
| 20 #include "chrome/common/notification_registrar.h" |
| 21 #include "skia/ext/skia_utils_mac.h" |
| 22 |
| 23 static const CGFloat kBrowserActionBadgeOriginYOffset = -4; |
| 24 |
| 25 // Since the container is the maximum height of the toolbar, we have to move the |
| 26 // buttons up by this amount in order to have them look vertically centered |
| 27 // within the toolbar. |
| 28 static const CGFloat kBrowserActionOriginYOffset = 5; |
| 29 |
| 30 // The size of each button on the toolbar. |
| 31 static const CGFloat kBrowserActionHeight = 27; |
| 32 extern const CGFloat kBrowserActionWidth = 29; |
| 33 |
| 34 // The padding between browser action buttons. |
| 35 extern const CGFloat kBrowserActionButtonPadding = 3; |
| 36 |
| 37 NSString* const kBrowserActionsChangedNotification = @"BrowserActionsChanged"; |
| 38 |
| 39 @interface BrowserActionBadgeView : NSView { |
| 40 @private |
| 41 // The current tab ID used when drawing the badge. |
| 42 int tabId_; |
| 43 |
| 44 // The action we're drawing the badge for. Weak. |
| 45 ExtensionAction* extensionAction_; |
| 46 } |
| 47 |
| 48 @property(readwrite, nonatomic) int tabId; |
| 49 @property(readwrite, nonatomic) ExtensionAction* extensionAction; |
| 50 |
| 51 @end |
| 52 |
| 53 @implementation BrowserActionBadgeView |
| 54 |
| 55 - (void)drawRect:(NSRect)dirtyRect { |
| 56 // CanvasPaint draws its content to the current NSGraphicsContext in its |
| 57 // destructor. If anything needs to be drawn afterwards, then enclose this |
| 58 // in a nested block. |
| 59 NSRect badgeBounds = [self bounds]; |
| 60 badgeBounds.origin.y += kBrowserActionBadgeOriginYOffset; |
| 61 gfx::CanvasPaint canvas(badgeBounds, false); |
| 62 canvas.set_composite_alpha(true); |
| 63 gfx::Rect boundingRect(NSRectToCGRect(badgeBounds)); |
| 64 extensionAction_->PaintBadge(&canvas, boundingRect, tabId_); |
| 65 } |
| 66 |
| 67 @synthesize tabId = tabId_; |
| 68 @synthesize extensionAction = extensionAction_; |
| 69 |
| 70 @end |
| 71 |
| 72 class ExtensionImageTrackerBridge; |
| 73 |
| 74 @interface BrowserActionButton : NSButton { |
| 75 @private |
| 76 scoped_ptr<ExtensionImageTrackerBridge> imageLoadingBridge_; |
| 77 |
| 78 scoped_nsobject<NSImage> defaultIcon_; |
| 79 |
| 80 scoped_nsobject<NSImage> tabSpecificIcon_; |
| 81 |
| 82 scoped_nsobject<NSView> badgeView_; |
| 83 |
| 84 // The extension for this button. Weak. |
| 85 Extension* extension_; |
| 86 |
| 87 // Weak. Owns us. |
| 88 BrowserActionsController* controller_; |
| 89 } |
| 90 |
| 91 - (id)initWithExtension:(Extension*)extension |
| 92 controller:(BrowserActionsController*)controller |
| 93 xOffset:(int)xOffset; |
| 94 |
| 95 - (void)setDefaultIcon:(NSImage*)image; |
| 96 |
| 97 - (void)setTabSpecificIcon:(NSImage*)image; |
| 98 |
| 99 - (void)updateState; |
| 100 |
| 101 @property(readonly, nonatomic) Extension* extension; |
| 102 |
| 103 @end |
| 104 |
| 105 // A helper class to bridge the asynchronous Skia bitmap loading mechanism to |
| 106 // the extension's button. |
| 107 class ExtensionImageTrackerBridge : public NotificationObserver, |
| 108 public ImageLoadingTracker::Observer { |
| 109 public: |
| 110 ExtensionImageTrackerBridge(BrowserActionButton* owner, Extension* extension) |
| 111 : owner_(owner), |
| 112 tracker_(NULL) { |
| 113 // The Browser Action API does not allow the default icon path to be |
| 114 // changed at runtime, so we can load this now and cache it. |
| 115 std::string path = extension->browser_action()->default_icon_path(); |
| 116 if (!path.empty()) { |
| 117 tracker_ = new ImageLoadingTracker(this, 1); |
| 118 tracker_->PostLoadImageTask(extension->GetResource(path), |
| 119 gfx::Size(Extension::kBrowserActionIconMaxSize, |
| 120 Extension::kBrowserActionIconMaxSize)); |
| 121 } |
| 122 registrar_.Add(this, NotificationType::EXTENSION_BROWSER_ACTION_UPDATED, |
| 123 Source<ExtensionAction>(extension->browser_action())); |
| 124 } |
| 125 |
| 126 ~ExtensionImageTrackerBridge() { |
| 127 if (tracker_) |
| 128 tracker_->StopTrackingImageLoad(); |
| 129 } |
| 130 |
| 131 // ImageLoadingTracker::Observer implementation. |
| 132 void OnImageLoaded(SkBitmap* image, size_t index) { |
| 133 if (image) |
| 134 [owner_ setDefaultIcon:gfx::SkBitmapToNSImage(*image)]; |
| 135 tracker_ = NULL; |
| 136 [owner_ updateState]; |
| 137 } |
| 138 |
| 139 // Overridden from NotificationObserver. |
| 140 void Observe(NotificationType type, |
| 141 const NotificationSource& source, |
| 142 const NotificationDetails& details) { |
| 143 if (type == NotificationType::EXTENSION_BROWSER_ACTION_UPDATED) |
| 144 [owner_ updateState]; |
| 145 else |
| 146 NOTREACHED(); |
| 147 } |
| 148 |
| 149 private: |
| 150 // Weak. Owns us. |
| 151 BrowserActionButton* owner_; |
| 152 |
| 153 // Loads the button's icons for us on the file thread. Weak. |
| 154 ImageLoadingTracker* tracker_; |
| 155 |
| 156 // Used for registering to receive notifications and automatic clean up. |
| 157 NotificationRegistrar registrar_; |
| 158 |
| 159 DISALLOW_COPY_AND_ASSIGN(ExtensionImageTrackerBridge); |
| 160 }; |
| 161 |
| 162 @implementation BrowserActionButton |
| 163 |
| 164 - (id)initWithExtension:(Extension*)extension |
| 165 controller:(BrowserActionsController*)controller |
| 166 xOffset:(int)xOffset { |
| 167 NSRect frame = NSMakeRect(xOffset, |
| 168 kBrowserActionOriginYOffset, |
| 169 kBrowserActionWidth, |
| 170 kBrowserActionHeight); |
| 171 if ((self = [super initWithFrame:frame])) { |
| 172 ToolbarButtonCell* cell = [[[ToolbarButtonCell alloc] init] autorelease]; |
| 173 // [NSButton setCell:] warns to NOT use setCell: other than in the |
| 174 // initializer of a control. However, we are using a basic |
| 175 // NSButton whose initializer does not take an NSCell as an |
| 176 // object. To honor the assumed semantics, we do nothing with |
| 177 // NSButton between alloc/init and setCell:. |
| 178 [self setCell:cell]; |
| 179 [self setTitle:@""]; |
| 180 [self setButtonType:NSMomentaryChangeButton]; |
| 181 [self setShowsBorderOnlyWhileMouseInside:YES]; |
| 182 |
| 183 [self setTarget:controller]; |
| 184 [self setAction:@selector(browserActionClicked:)]; |
| 185 |
| 186 extension_ = extension; |
| 187 controller_ = controller; |
| 188 imageLoadingBridge_.reset(new ExtensionImageTrackerBridge(self, extension)); |
| 189 |
| 190 NSRect badgeFrame = [self bounds]; |
| 191 badgeView_.reset([[BrowserActionBadgeView alloc] initWithFrame:badgeFrame]); |
| 192 [badgeView_ setTabId:[controller currentTabId]]; |
| 193 [badgeView_ setExtensionAction:extension->browser_action()]; |
| 194 [self addSubview:badgeView_]; |
| 195 |
| 196 [self updateState]; |
| 197 } |
| 198 |
| 199 return self; |
| 200 } |
| 201 |
| 202 - (void)setDefaultIcon:(NSImage*)image { |
| 203 defaultIcon_.reset([image retain]); |
| 204 } |
| 205 |
| 206 - (void)setTabSpecificIcon:(NSImage*)image { |
| 207 tabSpecificIcon_.reset([image retain]); |
| 208 } |
| 209 |
| 210 - (void)updateState { |
| 211 int tabId = [controller_ currentTabId]; |
| 212 if (tabId < 0) |
| 213 return; |
| 214 |
| 215 std::string tooltip = extension_->browser_action()->GetTitle(tabId); |
| 216 if (!tooltip.empty()) |
| 217 [self setToolTip:base::SysUTF8ToNSString(tooltip)]; |
| 218 |
| 219 SkBitmap image = extension_->browser_action()->GetIcon(tabId); |
| 220 if (!image.isNull()) { |
| 221 [self setTabSpecificIcon:gfx::SkBitmapToNSImage(image)]; |
| 222 [self setImage:tabSpecificIcon_]; |
| 223 } else if (defaultIcon_) { |
| 224 [self setImage:defaultIcon_]; |
| 225 } |
| 226 |
| 227 [badgeView_ setTabId:tabId]; |
| 228 |
| 229 [self setNeedsDisplay:YES]; |
| 230 } |
| 231 |
| 232 @synthesize extension = extension_; |
| 233 |
| 234 @end |
| 235 |
| 236 @interface BrowserActionsController(Private) |
| 237 |
| 238 - (void)createActionButtonForExtension:(Extension*)extension; |
| 239 - (void)removeActionButtonForExtension:(Extension*)extension; |
| 240 - (void)repositionActionButtons; |
| 241 |
| 242 @end |
| 243 |
| 244 // A helper class to proxy extension notifications to the view controller's |
| 245 // appropriate methods. |
| 246 class ExtensionsServiceObserverBridge : public NotificationObserver { |
| 247 public: |
| 248 ExtensionsServiceObserverBridge(BrowserActionsController* owner, |
| 249 Profile* profile) : owner_(owner) { |
| 250 registrar_.Add(this, NotificationType::EXTENSION_LOADED, |
| 251 Source<Profile>(profile)); |
| 252 registrar_.Add(this, NotificationType::EXTENSION_UNLOADED, |
| 253 Source<Profile>(profile)); |
| 254 registrar_.Add(this, NotificationType::EXTENSION_UNLOADED_DISABLED, |
| 255 Source<Profile>(profile)); |
| 256 registrar_.Add(this, NotificationType::EXTENSION_HOST_VIEW_SHOULD_CLOSE, |
| 257 Source<Profile>(profile)); |
| 258 } |
| 259 |
| 260 // Runs |owner_|'s method corresponding to the event type received from the |
| 261 // notification system. |
| 262 // Overridden from NotificationObserver. |
| 263 void Observe(NotificationType type, |
| 264 const NotificationSource& source, |
| 265 const NotificationDetails& details) { |
| 266 switch (type.value) { |
| 267 case NotificationType::EXTENSION_LOADED: { |
| 268 Extension* extension = Details<Extension>(details).ptr(); |
| 269 [owner_ createActionButtonForExtension:extension]; |
| 270 [owner_ browserActionVisibilityHasChanged]; |
| 271 break; |
| 272 } |
| 273 case NotificationType::EXTENSION_UNLOADED: |
| 274 case NotificationType::EXTENSION_UNLOADED_DISABLED: { |
| 275 Extension* extension = Details<Extension>(details).ptr(); |
| 276 [owner_ removeActionButtonForExtension:extension]; |
| 277 [owner_ browserActionVisibilityHasChanged]; |
| 278 break; |
| 279 } |
| 280 case NotificationType::EXTENSION_HOST_VIEW_SHOULD_CLOSE: |
| 281 //if (Details<ExtensionHost>(popup_->host()) != details) |
| 282 // return; |
| 283 [owner_ hidePopup]; |
| 284 break; |
| 285 default: |
| 286 NOTREACHED() << L"Unexpected notification"; |
| 287 } |
| 288 } |
| 289 |
| 290 private: |
| 291 // The object we need to inform when we get a notification. Weak. Owns us. |
| 292 BrowserActionsController* owner_; |
| 293 |
| 294 // Used for registering to receive notifications and automatic clean up. |
| 295 NotificationRegistrar registrar_; |
| 296 |
| 297 DISALLOW_COPY_AND_ASSIGN(ExtensionsServiceObserverBridge); |
| 298 }; |
| 299 |
| 300 @implementation BrowserActionsController |
| 301 |
| 302 - (id)initWithBrowser:(Browser*)browser |
| 303 containerView:(NSView*)container { |
| 304 DCHECK(browser && container); |
| 305 |
| 306 if ((self = [super init])) { |
| 307 browser_ = browser; |
| 308 profile_ = browser->profile(); |
| 309 |
| 310 containerView_ = container; |
| 311 [containerView_ setHidden:YES]; |
| 312 observer_.reset(new ExtensionsServiceObserverBridge(self, profile_)); |
| 313 buttons_.reset([[NSMutableDictionary alloc] init]); |
| 314 buttonOrder_.reset([[NSMutableArray alloc] init]); |
| 315 } |
| 316 |
| 317 return self; |
| 318 } |
| 319 |
| 320 - (void)hidePopup { |
| 321 NOTIMPLEMENTED(); |
| 322 } |
| 323 |
| 324 - (void)browserActionVisibilityHasChanged { |
| 325 [containerView_ setNeedsDisplay:YES]; |
| 326 } |
| 327 |
| 328 - (void)createButtons { |
| 329 ExtensionsService* extensionsService = profile_->GetExtensionsService(); |
| 330 if (!extensionsService) // |extensionsService| can be NULL in Incognito. |
| 331 return; |
| 332 |
| 333 for (size_t i = 0; i < extensionsService->extensions()->size(); ++i) { |
| 334 Extension* extension = extensionsService->GetExtensionById( |
| 335 extensionsService->extensions()->at(i)->id(), false); |
| 336 if (extension->browser_action()) { |
| 337 [self createActionButtonForExtension:extension]; |
| 338 } |
| 339 } |
| 340 } |
| 341 |
| 342 - (void)createActionButtonForExtension:(Extension*)extension { |
| 343 if (!extension->browser_action()) |
| 344 return; |
| 345 |
| 346 if ([buttons_ count] == 0) { |
| 347 // Only call if we're adding our first button, otherwise it will be shown |
| 348 // already. |
| 349 [containerView_ setHidden:NO]; |
| 350 } |
| 351 |
| 352 int xOffset = |
| 353 [buttons_ count] * (kBrowserActionWidth + kBrowserActionButtonPadding); |
| 354 BrowserActionButton* newButton = |
| 355 [[[BrowserActionButton alloc] initWithExtension:extension |
| 356 controller:self |
| 357 xOffset:xOffset] autorelease]; |
| 358 NSString* buttonKey = base::SysUTF8ToNSString(extension->id()); |
| 359 [buttons_ setObject:newButton forKey:buttonKey]; |
| 360 [buttonOrder_ addObject:newButton]; |
| 361 [containerView_ addSubview:newButton]; |
| 362 |
| 363 [[NSNotificationCenter defaultCenter] |
| 364 postNotificationName:kBrowserActionsChangedNotification object:self]; |
| 365 } |
| 366 |
| 367 - (void)removeActionButtonForExtension:(Extension*)extension { |
| 368 NSString* buttonKey = base::SysUTF8ToNSString(extension->id()); |
| 369 |
| 370 BrowserActionButton* button = [buttons_ objectForKey:buttonKey]; |
| 371 [button removeFromSuperview]; |
| 372 [buttons_ removeObjectForKey:buttonKey]; |
| 373 [buttonOrder_ removeObject:button]; |
| 374 if ([buttons_ count] == 0) { |
| 375 // No more buttons? Hide the container. |
| 376 [containerView_ setHidden:YES]; |
| 377 } else { |
| 378 // repositionActionButtons only needs to be called if removing a browser |
| 379 // action button because adding one will always append to the end of the |
| 380 // container, while removing one may require that those to the right of it |
| 381 // be shifted to the left. |
| 382 [self repositionActionButtons]; |
| 383 } |
| 384 [[NSNotificationCenter defaultCenter] |
| 385 postNotificationName:kBrowserActionsChangedNotification object:self]; |
| 386 } |
| 387 |
| 388 - (void)repositionActionButtons { |
| 389 for (NSUInteger i = 0; i < [buttonOrder_ count]; ++i) { |
| 390 CGFloat xOffset = i * (kBrowserActionWidth + kBrowserActionButtonPadding); |
| 391 BrowserActionButton* button = [buttonOrder_ objectAtIndex:i]; |
| 392 NSRect buttonFrame = [button frame]; |
| 393 buttonFrame.origin.x = xOffset; |
| 394 [button setFrame:buttonFrame]; |
| 395 } |
| 396 } |
| 397 |
| 398 - (int)buttonCount { |
| 399 return [buttons_ count]; |
| 400 } |
| 401 |
| 402 - (void)browserActionClicked:(BrowserActionButton*)sender { |
| 403 ExtensionAction* action = [sender extension]->browser_action(); |
| 404 if (action->has_popup()) { |
| 405 // Popups are not implemented for Mac yet. |
| 406 NOTIMPLEMENTED(); |
| 407 } else { |
| 408 ExtensionBrowserEventRouter::GetInstance()->BrowserActionExecuted( |
| 409 profile_, action->extension_id(), browser_); |
| 410 } |
| 411 } |
| 412 |
| 413 - (int)currentTabId { |
| 414 TabContents* selected_tab = browser_->GetSelectedTabContents(); |
| 415 if (!selected_tab) |
| 416 return -1; |
| 417 |
| 418 return selected_tab->controller().session_id().id(); |
| 419 } |
| 420 |
| 421 @end |
| OLD | NEW |