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 <cmath> 6 7 #import "chrome/browser/ui/cocoa/location_bar/page_action_decoration.h" 8 9 #include "base/sys_string_conversions.h" 10 #include "chrome/browser/extensions/extension_browser_event_router.h" 11 #include "chrome/browser/extensions/extension_service.h" 12 #include "chrome/browser/profiles/profile.h" 13 #import "chrome/browser/ui/cocoa/extensions/extension_action_context_menu.h" 14 #import "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h" 15 #import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h" 16 #include "chrome/common/extensions/extension_action.h" 17 #include "chrome/common/extensions/extension_resource.h" 18 #include "content/browser/tab_contents/tab_contents.h" 19 #include "content/common/notification_service.h" 20 #include "skia/ext/skia_utils_mac.h" 21 22 namespace { 23 24 // Distance to offset the bubble pointer from the bottom of the max 25 // icon area of the decoration. This makes the popup's upper border 26 // 2px away from the omnibox's lower border (matches omnibox popup 27 // upper border). 28 const CGFloat kBubblePointYOffset = 2.0; 29 30 } // namespace 31 32 PageActionDecoration::PageActionDecoration( 33 LocationBarViewMac* owner, 34 Profile* profile, 35 ExtensionAction* page_action) 36 : owner_(NULL), 37 profile_(profile), 38 page_action_(page_action), 39 tracker_(this), 40 current_tab_id_(-1), 41 preview_enabled_(false) { 42 DCHECK(profile); 43 const Extension* extension = profile->GetExtensionService()-> 44 GetExtensionById(page_action->extension_id(), false); 45 DCHECK(extension); 46 47 // Load all the icons declared in the manifest. This is the contents of the 48 // icons array, plus the default_icon property, if any. 49 std::vector<std::string> icon_paths(*page_action->icon_paths()); 50 if (!page_action_->default_icon_path().empty()) 51 icon_paths.push_back(page_action_->default_icon_path()); 52 53 for (std::vector<std::string>::iterator iter = icon_paths.begin(); 54 iter != icon_paths.end(); ++iter) { 55 tracker_.LoadImage(extension, extension->GetResource(*iter), 56 gfx::Size(Extension::kPageActionIconMaxSize, 57 Extension::kPageActionIconMaxSize), 58 ImageLoadingTracker::DONT_CACHE); 59 } 60 61 registrar_.Add(this, NotificationType::EXTENSION_HOST_VIEW_SHOULD_CLOSE, 62 Source<Profile>(profile_)); 63 64 // We set the owner last of all so that we can determine whether we are in 65 // the process of initializing this class or not. 66 owner_ = owner; 67 } 68 69 PageActionDecoration::~PageActionDecoration() {} 70 71 // Always |kPageActionIconMaxSize| wide. |ImageDecoration| draws the 72 // image centered. 73 CGFloat PageActionDecoration::GetWidthForSpace(CGFloat width) { 74 return Extension::kPageActionIconMaxSize; 75 } 76 77 bool PageActionDecoration::AcceptsMousePress() { 78 return true; 79 } 80 81 // Either notify listeners or show a popup depending on the Page 82 // Action. 83 bool PageActionDecoration::OnMousePressed(NSRect frame) { 84 if (current_tab_id_ < 0) { 85 NOTREACHED() << "No current tab."; 86 // We don't want other code to try and handle this click. Returning true 87 // prevents this by indicating that we handled it. 88 return true; 89 } 90 91 if (page_action_->HasPopup(current_tab_id_)) { 92 // Anchor popup at the bottom center of the page action icon. 93 AutocompleteTextField* field = owner_->GetAutocompleteTextField(); 94 NSPoint anchor = GetBubblePointInFrame(frame); 95 anchor = [field convertPoint:anchor toView:nil]; 96 97 const GURL popup_url(page_action_->GetPopupUrl(current_tab_id_)); 98 [ExtensionPopupController showURL:popup_url 99 inBrowser:BrowserList::GetLastActive() 100 anchoredAt:anchor 101 arrowLocation:info_bubble::kTopRight 102 devMode:NO]; 103 } else { 104 ExtensionService* service = profile_->GetExtensionService(); 105 service->browser_event_router()->PageActionExecuted( 106 profile_, page_action_->extension_id(), page_action_->id(), 107 current_tab_id_, current_url_.spec(), 108 1); 109 } 110 return true; 111 } 112 113 void PageActionDecoration::OnImageLoaded( 114 SkBitmap* image, const ExtensionResource& resource, int index) { 115 // We loaded icons()->size() icons, plus one extra if the Page Action had 116 // a default icon. 117 int total_icons = static_cast<int>(page_action_->icon_paths()->size()); 118 if (!page_action_->default_icon_path().empty()) 119 total_icons++; 120 DCHECK(index < total_icons); 121 122 // Map the index of the loaded image back to its name. If we ever get an 123 // index greater than the number of icons, it must be the default icon. 124 if (image) { 125 if (index < static_cast<int>(page_action_->icon_paths()->size())) 126 page_action_icons_[page_action_->icon_paths()->at(index)] = *image; 127 else 128 page_action_icons_[page_action_->default_icon_path()] = *image; 129 } 130 131 // If we have no owner, that means this class is still being constructed and 132 // we should not UpdatePageActions, since it leads to the PageActions being 133 // destroyed again and new ones recreated (causing an infinite loop). 134 if (owner_) 135 owner_->UpdatePageActions(); 136 } 137 138 void PageActionDecoration::UpdateVisibility(TabContents* contents, 139 const GURL& url) { 140 // Save this off so we can pass it back to the extension when the action gets 141 // executed. See PageActionDecoration::OnMousePressed. 142 current_tab_id_ = contents ? ExtensionTabUtil::GetTabId(contents) : -1; 143 current_url_ = url; 144 145 bool visible = contents && 146 (preview_enabled_ || page_action_->GetIsVisible(current_tab_id_)); 147 if (visible) { 148 SetToolTip(page_action_->GetTitle(current_tab_id_)); 149 150 // Set the image. 151 // It can come from three places. In descending order of priority: 152 // - The developer can set it dynamically by path or bitmap. It will be in 153 // page_action_->GetIcon(). 154 // - The developer can set it dynamically by index. It will be in 155 // page_action_->GetIconIndex(). 156 // - It can be set in the manifest by path. It will be in page_action_-> 157 // default_icon_path(). 158 159 // First look for a dynamically set bitmap. 160 SkBitmap skia_icon = page_action_->GetIcon(current_tab_id_); 161 if (skia_icon.isNull()) { 162 int icon_index = page_action_->GetIconIndex(current_tab_id_); 163 std::string icon_path = (icon_index < 0) ? 164 page_action_->default_icon_path() : 165 page_action_->icon_paths()->at(icon_index); 166 if (!icon_path.empty()) { 167 PageActionMap::iterator iter = page_action_icons_.find(icon_path); 168 if (iter != page_action_icons_.end()) 169 skia_icon = iter->second; 170 } 171 } 172 if (!skia_icon.isNull()) { 173 SetImage(gfx::SkBitmapToNSImage(skia_icon)); 174 } else if (!GetImage()) { 175 // During install the action can be displayed before the icons 176 // have come in. Rather than deal with this in multiple places, 177 // provide a placeholder image. This will be replaced when an 178 // icon comes in. 179 const NSSize default_size = NSMakeSize(Extension::kPageActionIconMaxSize, 180 Extension::kPageActionIconMaxSize); 181 SetImage([[NSImage alloc] initWithSize:default_size]); 182 } 183 } 184 185 if (IsVisible() != visible) { 186 SetVisible(visible); 187 NotificationService::current()->Notify( 188 NotificationType::EXTENSION_PAGE_ACTION_VISIBILITY_CHANGED, 189 Source<ExtensionAction>(page_action_), 190 Details<TabContents>(contents)); 191 } 192 } 193 194 void PageActionDecoration::SetToolTip(NSString* tooltip) { 195 tooltip_.reset([tooltip retain]); 196 } 197 198 void PageActionDecoration::SetToolTip(std::string tooltip) { 199 SetToolTip(tooltip.empty() ? nil : base::SysUTF8ToNSString(tooltip)); 200 } 201 202 NSString* PageActionDecoration::GetToolTip() { 203 return tooltip_.get(); 204 } 205 206 NSPoint PageActionDecoration::GetBubblePointInFrame(NSRect frame) { 207 // This is similar to |ImageDecoration::GetDrawRectInFrame()|, 208 // except that code centers the image, which can differ in size 209 // between actions. This centers the maximum image size, so the 210 // point will consistently be at the same y position. x position is 211 // easier (the middle of the centered image is the middle of the 212 // frame). 213 const CGFloat delta_height = 214 NSHeight(frame) - Extension::kPageActionIconMaxSize; 215 const CGFloat bottom_inset = std::ceil(delta_height / 2.0); 216 217 // Return a point just below the bottom of the maximal drawing area. 218 return NSMakePoint(NSMidX(frame), 219 NSMaxY(frame) - bottom_inset + kBubblePointYOffset); 220 } 221 222 NSMenu* PageActionDecoration::GetMenu() { 223 if (!profile_) 224 return nil; 225 ExtensionService* service = profile_->GetExtensionService(); 226 if (!service) 227 return nil; 228 const Extension* extension = service->GetExtensionById( 229 page_action_->extension_id(), false); 230 DCHECK(extension); 231 if (!extension) 232 return nil; 233 menu_.reset([[ExtensionActionContextMenu alloc] 234 initWithExtension:extension 235 profile:profile_ 236 extensionAction:page_action_]); 237 238 return menu_.get(); 239 } 240 241 void PageActionDecoration::Observe( 242 NotificationType type, 243 const NotificationSource& source, 244 const NotificationDetails& details) { 245 switch (type.value) { 246 case NotificationType::EXTENSION_HOST_VIEW_SHOULD_CLOSE: { 247 ExtensionPopupController* popup = [ExtensionPopupController popup]; 248 if (popup && ![popup isClosing]) 249 [popup close]; 250 251 break; 252 } 253 default: 254 NOTREACHED() << "Unexpected notification"; 255 break; 256 } 257 } 258