page_action_decoration.mm revision ddb351dbec246cf1fab5ec20d2d5520909041de1
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
22namespace {
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).
28const CGFloat kBubblePointYOffset = 2.0;
29
30}  // namespace
31
32PageActionDecoration::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
69PageActionDecoration::~PageActionDecoration() {}
70
71// Always |kPageActionIconMaxSize| wide.  |ImageDecoration| draws the
72// image centered.
73CGFloat PageActionDecoration::GetWidthForSpace(CGFloat width) {
74  return Extension::kPageActionIconMaxSize;
75}
76
77bool PageActionDecoration::AcceptsMousePress() {
78  return true;
79}
80
81// Either notify listeners or show a popup depending on the Page
82// Action.
83bool 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
113void 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
138void 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
194void PageActionDecoration::SetToolTip(NSString* tooltip) {
195  tooltip_.reset([tooltip retain]);
196}
197
198void PageActionDecoration::SetToolTip(std::string tooltip) {
199  SetToolTip(tooltip.empty() ? nil : base::SysUTF8ToNSString(tooltip));
200}
201
202NSString* PageActionDecoration::GetToolTip() {
203  return tooltip_.get();
204}
205
206NSPoint 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
222NSMenu* 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
241void 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