page_action_decoration.mm revision 5f1c94371a64b3196d4be9466099bb892df9b88e
1// Copyright (c) 2012 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/strings/sys_string_conversions.h"
10#include "chrome/browser/chrome_notification_types.h"
11#include "chrome/browser/extensions/extension_action.h"
12#include "chrome/browser/extensions/extension_tab_util.h"
13#include "chrome/browser/extensions/location_bar_controller.h"
14#include "chrome/browser/extensions/tab_helper.h"
15#include "chrome/browser/profiles/profile.h"
16#include "chrome/browser/sessions/session_id.h"
17#include "chrome/browser/ui/browser.h"
18#include "chrome/browser/ui/browser_window.h"
19#import "chrome/browser/ui/cocoa/extensions/extension_action_context_menu_controller.h"
20#import "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h"
21#include "chrome/browser/ui/cocoa/last_active_browser_cocoa.h"
22#import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
23#include "chrome/browser/ui/webui/extensions/extension_info_ui.h"
24#include "content/public/browser/notification_service.h"
25#include "content/public/browser/web_contents.h"
26#include "extensions/browser/extension_registry.h"
27#include "extensions/common/manifest_handlers/icons_handler.h"
28#include "skia/ext/skia_utils_mac.h"
29#include "ui/gfx/canvas_skia_paint.h"
30#include "ui/gfx/image/image.h"
31
32using content::WebContents;
33using extensions::Extension;
34using extensions::LocationBarController;
35
36namespace {
37
38// Distance to offset the bubble pointer from the bottom of the max
39// icon area of the decoration.  This makes the popup's upper border
40// 2px away from the omnibox's lower border (matches omnibox popup
41// upper border).
42const CGFloat kBubblePointYOffset = 2.0;
43
44}  // namespace
45
46PageActionDecoration::PageActionDecoration(
47    LocationBarViewMac* owner,
48    Browser* browser,
49    ExtensionAction* page_action)
50    : owner_(NULL),
51      browser_(browser),
52      page_action_(page_action),
53      current_tab_id_(-1),
54      preview_enabled_(false) {
55  const Extension* extension = extensions::ExtensionRegistry::Get(
56      browser->profile())->enabled_extensions().GetByID(
57          page_action->extension_id());
58  DCHECK(extension);
59
60  icon_factory_.reset(new ExtensionActionIconFactory(
61      browser_->profile(), extension, page_action, this));
62
63  registrar_.Add(this,
64                 extensions::NOTIFICATION_EXTENSION_HOST_VIEW_SHOULD_CLOSE,
65                 content::Source<Profile>(browser_->profile()));
66  registrar_.Add(this,
67                 extensions::NOTIFICATION_EXTENSION_COMMAND_PAGE_ACTION_MAC,
68                 content::Source<Profile>(browser_->profile()));
69
70  // We set the owner last of all so that we can determine whether we are in
71  // the process of initializing this class or not.
72  owner_ = owner;
73}
74
75PageActionDecoration::~PageActionDecoration() {}
76
77// Always |kPageActionIconMaxSize| wide.  |ImageDecoration| draws the
78// image centered.
79CGFloat PageActionDecoration::GetWidthForSpace(CGFloat width) {
80  return ExtensionAction::kPageActionIconMaxSize;
81}
82
83bool PageActionDecoration::AcceptsMousePress() {
84  return true;
85}
86
87// Either notify listeners or show a popup depending on the Page
88// Action.
89bool PageActionDecoration::OnMousePressed(NSRect frame, NSPoint location) {
90  return ActivatePageAction(frame);
91}
92
93void PageActionDecoration::ActivatePageAction() {
94  ActivatePageAction(owner_->GetPageActionFrame(page_action_));
95}
96
97bool PageActionDecoration::ActivatePageAction(NSRect frame) {
98  WebContents* web_contents = owner_->GetWebContents();
99  if (!web_contents) {
100    // We don't want other code to try and handle this click. Returning true
101    // prevents this by indicating that we handled it.
102    return true;
103  }
104
105  LocationBarController* controller =
106      extensions::TabHelper::FromWebContents(web_contents)->
107          location_bar_controller();
108
109  switch (controller->OnClicked(page_action_)) {
110    case LocationBarController::ACTION_NONE:
111      break;
112
113    case LocationBarController::ACTION_SHOW_POPUP:
114      ShowPopup(frame, page_action_->GetPopupUrl(current_tab_id_));
115      break;
116
117    case LocationBarController::ACTION_SHOW_CONTEXT_MENU:
118      // We are never passing OnClicked a right-click button, so assume that
119      // we're never going to be asked to show a context menu.
120      // TODO(kalman): if this changes, update this class to pass the real
121      // mouse button through to the LocationBarController.
122      NOTREACHED();
123      break;
124  }
125
126  return true;
127}
128
129void PageActionDecoration::OnIconUpdated() {
130  // If we have no owner, that means this class is still being constructed.
131  WebContents* web_contents = owner_ ? owner_->GetWebContents() : NULL;
132  if (web_contents) {
133    UpdateVisibility(web_contents, current_url_);
134    owner_->RedrawDecoration(this);
135  }
136}
137
138void PageActionDecoration::UpdateVisibility(WebContents* 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_ =
143      contents ? extensions::ExtensionTabUtil::GetTabId(contents) : -1;
144  current_url_ = url;
145
146  bool visible = contents &&
147      (preview_enabled_ || page_action_->GetIsVisible(current_tab_id_));
148  if (visible) {
149    SetToolTip(page_action_->GetTitle(current_tab_id_));
150
151    // Set the image.
152    gfx::Image icon = icon_factory_->GetIcon(current_tab_id_);
153    if (!icon.IsEmpty()) {
154      SetImage(icon.ToNSImage());
155    } else if (!GetImage()) {
156      const NSSize default_size = NSMakeSize(
157          ExtensionAction::kPageActionIconMaxSize,
158          ExtensionAction::kPageActionIconMaxSize);
159      SetImage([[[NSImage alloc] initWithSize:default_size] autorelease]);
160    }
161  }
162
163  if (IsVisible() != visible) {
164    SetVisible(visible);
165    content::NotificationService::current()->Notify(
166        extensions::NOTIFICATION_EXTENSION_PAGE_ACTION_VISIBILITY_CHANGED,
167        content::Source<ExtensionAction>(page_action_),
168        content::Details<WebContents>(contents));
169  }
170}
171
172void PageActionDecoration::SetToolTip(NSString* tooltip) {
173  tooltip_.reset([tooltip retain]);
174}
175
176void PageActionDecoration::SetToolTip(std::string tooltip) {
177  SetToolTip(tooltip.empty() ? nil : base::SysUTF8ToNSString(tooltip));
178}
179
180NSString* PageActionDecoration::GetToolTip() {
181  return tooltip_.get();
182}
183
184NSPoint PageActionDecoration::GetBubblePointInFrame(NSRect frame) {
185  // This is similar to |ImageDecoration::GetDrawRectInFrame()|,
186  // except that code centers the image, which can differ in size
187  // between actions.  This centers the maximum image size, so the
188  // point will consistently be at the same y position.  x position is
189  // easier (the middle of the centered image is the middle of the
190  // frame).
191  const CGFloat delta_height =
192      NSHeight(frame) - ExtensionAction::kPageActionIconMaxSize;
193  const CGFloat bottom_inset = std::ceil(delta_height / 2.0);
194
195  // Return a point just below the bottom of the maximal drawing area.
196  return NSMakePoint(NSMidX(frame),
197                     NSMaxY(frame) - bottom_inset + kBubblePointYOffset);
198}
199
200NSMenu* PageActionDecoration::GetMenu() {
201  const Extension* extension = extensions::ExtensionRegistry::Get(
202      browser_->profile())->enabled_extensions().GetByID(
203          page_action_->extension_id());
204  DCHECK(extension);
205  if (!extension->ShowConfigureContextMenus())
206    return nil;
207
208  contextMenuController_.reset([[ExtensionActionContextMenuController alloc]
209      initWithExtension:extension
210                browser:browser_
211        extensionAction:page_action_]);
212
213  base::scoped_nsobject<NSMenu> contextMenu([[NSMenu alloc] initWithTitle:@""]);
214  [contextMenuController_ populateMenu:contextMenu];
215  return contextMenu.autorelease();
216}
217
218void PageActionDecoration::ShowPopup(const NSRect& frame,
219                                     const GURL& popup_url) {
220  // Anchor popup at the bottom center of the page action icon.
221  AutocompleteTextField* field = owner_->GetAutocompleteTextField();
222  NSPoint anchor = GetBubblePointInFrame(frame);
223  anchor = [field convertPoint:anchor toView:nil];
224
225  [ExtensionPopupController showURL:popup_url
226                          inBrowser:chrome::GetLastActiveBrowser()
227                         anchoredAt:anchor
228                      arrowLocation:info_bubble::kTopRight
229                            devMode:NO];
230}
231
232void PageActionDecoration::Observe(
233    int type,
234    const content::NotificationSource& source,
235    const content::NotificationDetails& details) {
236  switch (type) {
237    case extensions::NOTIFICATION_EXTENSION_HOST_VIEW_SHOULD_CLOSE: {
238      ExtensionPopupController* popup = [ExtensionPopupController popup];
239      if (popup && ![popup isClosing])
240        [popup close];
241
242      break;
243    }
244    case extensions::NOTIFICATION_EXTENSION_COMMAND_PAGE_ACTION_MAC: {
245      std::pair<const std::string, gfx::NativeWindow>* payload =
246      content::Details<std::pair<const std::string, gfx::NativeWindow> >(
247          details).ptr();
248      std::string extension_id = payload->first;
249      gfx::NativeWindow window = payload->second;
250      if (window != browser_->window()->GetNativeWindow())
251        break;
252      if (extension_id != page_action_->extension_id())
253        break;
254      if (IsVisible())
255        ActivatePageAction();
256      break;
257    }
258
259    default:
260      NOTREACHED() << "Unexpected notification";
261      break;
262  }
263}
264