summaryrefslogtreecommitdiffstats
path: root/chrome/browser/tab_contents/render_view_context_menu_mac.mm
blob: 9e0f067060001ac3eeefe37250b87b29d7373bc8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/tab_contents/render_view_context_menu_mac.h"

#include "base/compiler_specific.h"
#import "base/mac/scoped_sending_event.h"
#include "base/memory/scoped_nsobject.h"
#include "base/message_loop.h"
#include "base/sys_string_conversions.h"
#include "chrome/app/chrome_command_ids.h"
#import "chrome/browser/ui/cocoa/browser_window_controller.h"
#import "chrome/browser/ui/cocoa/menu_controller.h"
#include "grit/generated_resources.h"
#include "ui/base/l10n/l10n_util.h"

using content::WebContents;

// These are not documented, so use only after checking -respondsToSelector:.
@interface NSApplication (UndocumentedSpeechMethods)
- (void)speakString:(NSString*)string;
- (void)stopSpeaking:(id)sender;
- (BOOL)isSpeaking;
@end

namespace {

// Retrieves an NSMenuItem which has the specified command_id. This function
// traverses the given |model| in the depth-first order. When this function
// finds an item whose command_id is the same as the given |command_id|, it
// returns the NSMenuItem associated with the item. This function emulates
// views::MenuItemViews::GetMenuItemByID() for Mac.
NSMenuItem* GetMenuItemByID(ui::MenuModel* model,
                            NSMenu* menu,
                            int command_id) {
  for (int i = 0; i < model->GetItemCount(); ++i) {
    NSMenuItem* item = [menu itemAtIndex:i];
    if (model->GetCommandIdAt(i) == command_id)
      return item;

    ui::MenuModel* submenu = model->GetSubmenuModelAt(i);
    if (submenu && [item hasSubmenu]) {
      NSMenuItem* subitem = GetMenuItemByID(submenu,
                                            [item submenu],
                                            command_id);
      if (subitem)
        return subitem;
    }
  }
  return nil;
}

}  // namespace

// Obj-C bridge class that is the target of all items in the context menu.
// Relies on the tag being set to the command id.

RenderViewContextMenuMac::RenderViewContextMenuMac(
    WebContents* web_contents,
    const content::ContextMenuParams& params,
    NSView* parent_view)
    : RenderViewContextMenu(web_contents, params),
      ALLOW_THIS_IN_INITIALIZER_LIST(speech_submenu_model_(this)),
      parent_view_(parent_view) {
}

RenderViewContextMenuMac::~RenderViewContextMenuMac() {
}

void RenderViewContextMenuMac::PlatformInit() {
  InitPlatformMenu();
  menu_controller_.reset(
      [[MenuController alloc] initWithModel:&menu_model_
                     useWithPopUpButtonCell:NO]);

  // Synthesize an event for the click, as there is no certainty that
  // [NSApp currentEvent] will return a valid event.
  NSEvent* currentEvent = [NSApp currentEvent];
  NSWindow* window = [parent_view_ window];
  NSPoint position = [window mouseLocationOutsideOfEventStream];
  NSTimeInterval eventTime = [currentEvent timestamp];
  NSEvent* clickEvent = [NSEvent mouseEventWithType:NSRightMouseDown
                                           location:position
                                      modifierFlags:NSRightMouseDownMask
                                          timestamp:eventTime
                                       windowNumber:[window windowNumber]
                                            context:nil
                                        eventNumber:0
                                         clickCount:1
                                           pressure:1.0];

  {
    // Make sure events can be pumped while the menu is up.
    MessageLoop::ScopedNestableTaskAllower allow(MessageLoop::current());

    // One of the events that could be pumped is |window.close()|.
    // User-initiated event-tracking loops protect against this by
    // setting flags in -[CrApplication sendEvent:], but since
    // web-content menus are initiated by IPC message the setup has to
    // be done manually.
    base::mac::ScopedSendingEvent sendingEventScoper;

    // Show the menu.
    [NSMenu popUpContextMenu:[menu_controller_ menu]
                   withEvent:clickEvent
                     forView:parent_view_];
  }
}

void RenderViewContextMenuMac::ExecuteCommand(int id) {
  // Auxiliary windows that do not have address bars (Panels for example)
  // may not have Instant support.
  NSWindow* parent_window = [parent_view_ window];
  BrowserWindowController* controller =
      [BrowserWindowController browserWindowControllerForWindow:parent_window];
  [controller commitInstant];  // It's ok if controller is nil.

  ExecuteCommand(id, 0);
}

void RenderViewContextMenuMac::ExecuteCommand(int command_id, int event_flags) {
  switch (command_id) {
    case IDC_CONTENT_CONTEXT_LOOK_UP_IN_DICTIONARY:
      LookUpInDictionary();
      break;

    case IDC_CONTENT_CONTEXT_SPEECH_START_SPEAKING:
      StartSpeaking();
      break;

    case IDC_CONTENT_CONTEXT_SPEECH_STOP_SPEAKING:
      StopSpeaking();
      break;

    default:
      RenderViewContextMenu::ExecuteCommand(command_id, event_flags);
      break;
  }
}

bool RenderViewContextMenuMac::IsCommandIdEnabled(int command_id) const {
  switch (command_id) {
    case IDC_CONTENT_CONTEXT_LOOK_UP_IN_DICTIONARY:
      // This is OK because the menu is not shown when it isn't
      // appropriate.
      return true;

    case IDC_CONTENT_CONTEXT_SPEECH_START_SPEAKING:
      // This is OK because the menu is not shown when it isn't
      // appropriate.
      return true;

    case IDC_CONTENT_CONTEXT_SPEECH_STOP_SPEAKING:
      return [NSApp respondsToSelector:@selector(isSpeaking)] &&
             [NSApp isSpeaking];

    default:
      return RenderViewContextMenu::IsCommandIdEnabled(command_id);
  }
}

bool RenderViewContextMenuMac::GetAcceleratorForCommandId(
    int command_id,
    ui::Accelerator* accelerator) {
  return false;
}

void RenderViewContextMenuMac::InitPlatformMenu() {
  bool has_selection = !params_.selection_text.empty();

  if (has_selection) {
    menu_model_.AddSeparator();
    menu_model_.AddItemWithStringId(
        IDC_CONTENT_CONTEXT_LOOK_UP_IN_DICTIONARY,
        IDS_CONTENT_CONTEXT_LOOK_UP_IN_DICTIONARY);

    // Add speech items only if NSApp supports the private API we're using.
    if ([NSApp respondsToSelector:@selector(speakString:)] &&
        [NSApp respondsToSelector:@selector(stopSpeaking:)]) {
      speech_submenu_model_.AddItemWithStringId(
          IDC_CONTENT_CONTEXT_SPEECH_START_SPEAKING,
          IDS_CONTENT_CONTEXT_SPEECH_START_SPEAKING);
      speech_submenu_model_.AddItemWithStringId(
          IDC_CONTENT_CONTEXT_SPEECH_STOP_SPEAKING,
          IDS_CONTENT_CONTEXT_SPEECH_STOP_SPEAKING);
      menu_model_.AddSubMenu(
          IDC_CONTENT_CONTEXT_SPEECH_MENU,
          l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_SPEECH_MENU),
          &speech_submenu_model_);
    }
  }
}

void RenderViewContextMenuMac::LookUpInDictionary() {
  // TODO(morrita): On Safari, A dictionary panel could be shown
  // based on a preference setting of Dictionary.app.  We currently
  // don't support it: http://crbug.com/17951
  NSString* text = base::SysUTF16ToNSString(params_.selection_text);
  NSPasteboard* pboard = [NSPasteboard pasteboardWithUniqueName];
  // 10.5 and earlier require declareTypes before setData.
  // See the documentation on [NSPasteboard declareTypes].
  NSArray* toDeclare = [NSArray arrayWithObject:NSStringPboardType];
  [pboard declareTypes:toDeclare owner:nil];
  BOOL ok = [pboard setString:text forType:NSStringPboardType];
  if (ok)
    NSPerformService(@"Look Up in Dictionary", pboard);
}

void RenderViewContextMenuMac::StartSpeaking() {
  NSString* text = base::SysUTF16ToNSString(params_.selection_text);
  [NSApp speakString:text];
}

void RenderViewContextMenuMac::StopSpeaking() {
  [NSApp stopSpeaking:menu_controller_];
}

void RenderViewContextMenuMac::UpdateMenuItem(int command_id,
                                              bool enabled,
                                              bool hidden,
                                              const string16& title) {
  NSMenuItem* item = GetMenuItemByID(&menu_model_, [menu_controller_ menu],
                                     command_id);
  if (!item)
    return;

  // Update the returned NSMenuItem directly so we can update it immediately.
  [item setEnabled:enabled];
  [item setTitle:SysUTF16ToNSString(title)];
  [item setHidden:hidden];
  [[item menu] itemChanged:item];
}