page_action_decoration.mm revision 21d179b334e59e9a3bfcaed4c4430bef1bc5759d
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 <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#include "chrome/browser/tab_contents/tab_contents.h"
14#import "chrome/browser/ui/cocoa/extensions/extension_action_context_menu.h"
15#import "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h"
16#import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
17#include "chrome/common/extensions/extension_action.h"
18#include "chrome/common/extensions/extension_resource.h"
19#include "chrome/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
77// Either notify listeners or show a popup depending on the Page
78// Action.
79bool PageActionDecoration::OnMousePressed(NSRect frame) {
80  if (current_tab_id_ < 0) {
81    NOTREACHED() << "No current tab.";
82    // We don't want other code to try and handle this click.  Returning true
83    // prevents this by indicating that we handled it.
84    return true;
85  }
86
87  if (page_action_->HasPopup(current_tab_id_)) {
88    // Anchor popup at the bottom center of the page action icon.
89    AutocompleteTextField* field = owner_->GetAutocompleteTextField();
90    NSPoint anchor = GetBubblePointInFrame(frame);
91    anchor = [field convertPoint:anchor toView:nil];
92
93    const GURL popup_url(page_action_->GetPopupUrl(current_tab_id_));
94    [ExtensionPopupController showURL:popup_url
95                            inBrowser:BrowserList::GetLastActive()
96                           anchoredAt:anchor
97                        arrowLocation:info_bubble::kTopRight
98                              devMode:NO];
99  } else {
100    ExtensionBrowserEventRouter::GetInstance()->PageActionExecuted(
101        profile_, page_action_->extension_id(), page_action_->id(),
102        current_tab_id_, current_url_.spec(),
103        1);
104  }
105  return true;
106}
107
108void PageActionDecoration::OnImageLoaded(
109    SkBitmap* image, ExtensionResource resource, int index) {
110  // We loaded icons()->size() icons, plus one extra if the Page Action had
111  // a default icon.
112  int total_icons = static_cast<int>(page_action_->icon_paths()->size());
113  if (!page_action_->default_icon_path().empty())
114    total_icons++;
115  DCHECK(index < total_icons);
116
117  // Map the index of the loaded image back to its name. If we ever get an
118  // index greater than the number of icons, it must be the default icon.
119  if (image) {
120    if (index < static_cast<int>(page_action_->icon_paths()->size()))
121      page_action_icons_[page_action_->icon_paths()->at(index)] = *image;
122    else
123      page_action_icons_[page_action_->default_icon_path()] = *image;
124  }
125
126  // If we have no owner, that means this class is still being constructed and
127  // we should not UpdatePageActions, since it leads to the PageActions being
128  // destroyed again and new ones recreated (causing an infinite loop).
129  if (owner_)
130    owner_->UpdatePageActions();
131}
132
133void PageActionDecoration::UpdateVisibility(TabContents* contents,
134                                            const GURL& url) {
135  // Save this off so we can pass it back to the extension when the action gets
136  // executed. See PageActionDecoration::OnMousePressed.
137  current_tab_id_ = contents ? ExtensionTabUtil::GetTabId(contents) : -1;
138  current_url_ = url;
139
140  bool visible = contents &&
141      (preview_enabled_ || page_action_->GetIsVisible(current_tab_id_));
142  if (visible) {
143    SetToolTip(page_action_->GetTitle(current_tab_id_));
144
145    // Set the image.
146    // It can come from three places. In descending order of priority:
147    // - The developer can set it dynamically by path or bitmap. It will be in
148    //   page_action_->GetIcon().
149    // - The developer can set it dynamically by index. It will be in
150    //   page_action_->GetIconIndex().
151    // - It can be set in the manifest by path. It will be in page_action_->
152    //   default_icon_path().
153
154    // First look for a dynamically set bitmap.
155    SkBitmap skia_icon = page_action_->GetIcon(current_tab_id_);
156    if (skia_icon.isNull()) {
157      int icon_index = page_action_->GetIconIndex(current_tab_id_);
158      std::string icon_path = (icon_index < 0) ?
159          page_action_->default_icon_path() :
160          page_action_->icon_paths()->at(icon_index);
161      if (!icon_path.empty()) {
162        PageActionMap::iterator iter = page_action_icons_.find(icon_path);
163        if (iter != page_action_icons_.end())
164          skia_icon = iter->second;
165      }
166    }
167    if (!skia_icon.isNull()) {
168      SetImage(gfx::SkBitmapToNSImage(skia_icon));
169    } else if (!GetImage()) {
170      // During install the action can be displayed before the icons
171      // have come in.  Rather than deal with this in multiple places,
172      // provide a placeholder image.  This will be replaced when an
173      // icon comes in.
174      const NSSize default_size = NSMakeSize(Extension::kPageActionIconMaxSize,
175                                             Extension::kPageActionIconMaxSize);
176      SetImage([[NSImage alloc] initWithSize:default_size]);
177    }
178  }
179
180  if (IsVisible() != visible) {
181    SetVisible(visible);
182    NotificationService::current()->Notify(
183        NotificationType::EXTENSION_PAGE_ACTION_VISIBILITY_CHANGED,
184        Source<ExtensionAction>(page_action_),
185        Details<TabContents>(contents));
186  }
187}
188
189void PageActionDecoration::SetToolTip(NSString* tooltip) {
190  tooltip_.reset([tooltip retain]);
191}
192
193void PageActionDecoration::SetToolTip(std::string tooltip) {
194  SetToolTip(tooltip.empty() ? nil : base::SysUTF8ToNSString(tooltip));
195}
196
197NSString* PageActionDecoration::GetToolTip() {
198  return tooltip_.get();
199}
200
201NSPoint PageActionDecoration::GetBubblePointInFrame(NSRect frame) {
202  // This is similar to |ImageDecoration::GetDrawRectInFrame()|,
203  // except that code centers the image, which can differ in size
204  // between actions.  This centers the maximum image size, so the
205  // point will consistently be at the same y position.  x position is
206  // easier (the middle of the centered image is the middle of the
207  // frame).
208  const CGFloat delta_height =
209      NSHeight(frame) - Extension::kPageActionIconMaxSize;
210  const CGFloat bottom_inset = std::ceil(delta_height / 2.0);
211
212  // Return a point just below the bottom of the maximal drawing area.
213  return NSMakePoint(NSMidX(frame),
214                     NSMaxY(frame) - bottom_inset + kBubblePointYOffset);
215}
216
217NSMenu* PageActionDecoration::GetMenu() {
218  if (!profile_)
219    return nil;
220  ExtensionService* service = profile_->GetExtensionService();
221  if (!service)
222    return nil;
223  const Extension* extension = service->GetExtensionById(
224      page_action_->extension_id(), false);
225  DCHECK(extension);
226  if (!extension)
227    return nil;
228  menu_.reset([[ExtensionActionContextMenu alloc]
229      initWithExtension:extension
230                profile:profile_
231        extensionAction:page_action_]);
232
233  return menu_.get();
234}
235
236void PageActionDecoration::Observe(
237    NotificationType type,
238    const NotificationSource& source,
239    const NotificationDetails& details) {
240  switch (type.value) {
241    case NotificationType::EXTENSION_HOST_VIEW_SHOULD_CLOSE: {
242      ExtensionPopupController* popup = [ExtensionPopupController popup];
243      if (popup && ![popup isClosing])
244        [popup close];
245
246      break;
247    }
248    default:
249      NOTREACHED() << "Unexpected notification";
250      break;
251  }
252}
253