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_service.h"
13#include "chrome/browser/extensions/extension_tab_util.h"
14#include "chrome/browser/extensions/location_bar_controller.h"
15#include "chrome/browser/extensions/tab_helper.h"
16#include "chrome/browser/profiles/profile.h"
17#include "chrome/browser/sessions/session_id.h"
18#include "chrome/browser/ui/browser.h"
19#include "chrome/browser/ui/browser_window.h"
20#import "chrome/browser/ui/cocoa/extensions/extension_action_context_menu_controller.h"
21#import "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h"
22#include "chrome/browser/ui/cocoa/last_active_browser_cocoa.h"
23#import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
24#include "chrome/browser/ui/omnibox/location_bar_util.h"
25#include "chrome/browser/ui/webui/extensions/extension_info_ui.h"
26#include "content/public/browser/notification_service.h"
27#include "content/public/browser/web_contents.h"
28#include "extensions/common/manifest_handlers/icons_handler.h"
29#include "skia/ext/skia_utils_mac.h"
30#include "ui/gfx/canvas_skia_paint.h"
31#include "ui/gfx/image/image.h"
32
33using content::WebContents;
34using extensions::Extension;
35using extensions::LocationBarController;
36
37namespace {
38
39// Distance to offset the bubble pointer from the bottom of the max
40// icon area of the decoration.  This makes the popup's upper border
41// 2px away from the omnibox's lower border (matches omnibox popup
42// upper border).
43const CGFloat kBubblePointYOffset = 2.0;
44
45}  // namespace
46
47PageActionDecoration::PageActionDecoration(
48    LocationBarViewMac* owner,
49    Browser* browser,
50    ExtensionAction* page_action)
51    : owner_(NULL),
52      browser_(browser),
53      page_action_(page_action),
54      current_tab_id_(-1),
55      preview_enabled_(false) {
56  const Extension* extension = browser->profile()->GetExtensionService()->
57      GetExtensionById(page_action->extension_id(), false);
58  DCHECK(extension);
59
60  icon_factory_.reset(new ExtensionActionIconFactory(
61      browser_->profile(), extension, page_action, this));
62
63  registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_HOST_VIEW_SHOULD_CLOSE,
64      content::Source<Profile>(browser_->profile()));
65  registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_COMMAND_PAGE_ACTION_MAC,
66      content::Source<Profile>(browser_->profile()));
67
68  // We set the owner last of all so that we can determine whether we are in
69  // the process of initializing this class or not.
70  owner_ = owner;
71}
72
73PageActionDecoration::~PageActionDecoration() {}
74
75// Always |kPageActionIconMaxSize| wide.  |ImageDecoration| draws the
76// image centered.
77CGFloat PageActionDecoration::GetWidthForSpace(CGFloat width) {
78  return ExtensionAction::kPageActionIconMaxSize;
79}
80
81bool PageActionDecoration::AcceptsMousePress() {
82  return true;
83}
84
85// Either notify listeners or show a popup depending on the Page
86// Action.
87bool PageActionDecoration::OnMousePressed(NSRect frame, NSPoint location) {
88  return ActivatePageAction(frame);
89}
90
91void PageActionDecoration::ActivatePageAction() {
92  ActivatePageAction(owner_->GetPageActionFrame(page_action_));
93}
94
95bool PageActionDecoration::ActivatePageAction(NSRect frame) {
96  WebContents* web_contents = owner_->GetWebContents();
97  if (!web_contents) {
98    // We don't want other code to try and handle this click. Returning true
99    // prevents this by indicating that we handled it.
100    return true;
101  }
102
103  LocationBarController* controller =
104      extensions::TabHelper::FromWebContents(web_contents)->
105          location_bar_controller();
106
107  switch (controller->OnClicked(page_action_)) {
108    case LocationBarController::ACTION_NONE:
109      break;
110
111    case LocationBarController::ACTION_SHOW_POPUP:
112      ShowPopup(frame, page_action_->GetPopupUrl(current_tab_id_));
113      break;
114
115    case LocationBarController::ACTION_SHOW_CONTEXT_MENU:
116      // We are never passing OnClicked a right-click button, so assume that
117      // we're never going to be asked to show a context menu.
118      // TODO(kalman): if this changes, update this class to pass the real
119      // mouse button through to the LocationBarController.
120      NOTREACHED();
121      break;
122  }
123
124  return true;
125}
126
127void PageActionDecoration::OnIconUpdated() {
128  // If we have no owner, that means this class is still being constructed.
129  WebContents* web_contents = owner_ ? owner_->GetWebContents() : NULL;
130  if (web_contents) {
131    UpdateVisibility(web_contents, current_url_);
132    owner_->RedrawDecoration(this);
133  }
134}
135
136void PageActionDecoration::UpdateVisibility(WebContents* contents,
137                                            const GURL& url) {
138  // Save this off so we can pass it back to the extension when the action gets
139  // executed. See PageActionDecoration::OnMousePressed.
140  current_tab_id_ =
141      contents ? extensions::ExtensionTabUtil::GetTabId(contents) : -1;
142  current_url_ = url;
143
144  bool visible = contents &&
145      (preview_enabled_ || page_action_->GetIsVisible(current_tab_id_));
146  if (visible) {
147    SetToolTip(page_action_->GetTitle(current_tab_id_));
148
149    // Set the image.
150    gfx::Image icon = icon_factory_->GetIcon(current_tab_id_);
151    if (!icon.IsEmpty()) {
152      SetImage(icon.ToNSImage());
153    } else if (!GetImage()) {
154      const NSSize default_size = NSMakeSize(
155          ExtensionAction::kPageActionIconMaxSize,
156          ExtensionAction::kPageActionIconMaxSize);
157      SetImage([[[NSImage alloc] initWithSize:default_size] autorelease]);
158    }
159  }
160
161  if (IsVisible() != visible) {
162    SetVisible(visible);
163    content::NotificationService::current()->Notify(
164        chrome::NOTIFICATION_EXTENSION_PAGE_ACTION_VISIBILITY_CHANGED,
165        content::Source<ExtensionAction>(page_action_),
166        content::Details<WebContents>(contents));
167  }
168}
169
170void PageActionDecoration::SetToolTip(NSString* tooltip) {
171  tooltip_.reset([tooltip retain]);
172}
173
174void PageActionDecoration::SetToolTip(std::string tooltip) {
175  SetToolTip(tooltip.empty() ? nil : base::SysUTF8ToNSString(tooltip));
176}
177
178NSString* PageActionDecoration::GetToolTip() {
179  return tooltip_.get();
180}
181
182NSPoint PageActionDecoration::GetBubblePointInFrame(NSRect frame) {
183  // This is similar to |ImageDecoration::GetDrawRectInFrame()|,
184  // except that code centers the image, which can differ in size
185  // between actions.  This centers the maximum image size, so the
186  // point will consistently be at the same y position.  x position is
187  // easier (the middle of the centered image is the middle of the
188  // frame).
189  const CGFloat delta_height =
190      NSHeight(frame) - ExtensionAction::kPageActionIconMaxSize;
191  const CGFloat bottom_inset = std::ceil(delta_height / 2.0);
192
193  // Return a point just below the bottom of the maximal drawing area.
194  return NSMakePoint(NSMidX(frame),
195                     NSMaxY(frame) - bottom_inset + kBubblePointYOffset);
196}
197
198NSMenu* PageActionDecoration::GetMenu() {
199  ExtensionService* service = browser_->profile()->GetExtensionService();
200  if (!service)
201    return nil;
202  const Extension* extension = service->GetExtensionById(
203      page_action_->extension_id(), false);
204  DCHECK(extension);
205  if (!extension || !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 chrome::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 chrome::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