1/*
2 * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 *
8 * 1.  Redistributions of source code must retain the above copyright
9 *     notice, this list of conditions and the following disclaimer.
10 * 2.  Redistributions in binary form must reproduce the above copyright
11 *     notice, this list of conditions and the following disclaimer in the
12 *     documentation and/or other materials provided with the distribution.
13 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14 *     its contributors may be used to endorse or promote products derived
15 *     from this software without specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 */
28
29#import "WebContextMenuClient.h"
30
31#import "WebDelegateImplementationCaching.h"
32#import "WebElementDictionary.h"
33#import "WebFrame.h"
34#import "WebFrameInternal.h"
35#import "WebHTMLView.h"
36#import "WebHTMLViewInternal.h"
37#import "WebKitVersionChecks.h"
38#import "WebNSPasteboardExtras.h"
39#import "WebUIDelegate.h"
40#import "WebUIDelegatePrivate.h"
41#import "WebView.h"
42#import "WebViewInternal.h"
43#import <WebCore/ContextMenu.h>
44#import <WebCore/ContextMenuController.h>
45#import <WebCore/Document.h>
46#import <WebCore/KURL.h>
47#import <WebCore/LocalizedStrings.h>
48#import <WebCore/Page.h>
49#import <WebCore/Frame.h>
50#import <WebCore/FrameView.h>
51#import <WebCore/RuntimeApplicationChecks.h>
52#import <WebKit/DOMPrivate.h>
53
54using namespace WebCore;
55
56@interface NSApplication (AppKitSecretsIKnowAbout)
57- (void)speakString:(NSString *)string;
58@end
59
60WebContextMenuClient::WebContextMenuClient(WebView *webView)
61    : m_webView(webView)
62{
63}
64
65void WebContextMenuClient::contextMenuDestroyed()
66{
67    delete this;
68}
69
70static BOOL isPreVersion3Client(void)
71{
72    static BOOL preVersion3Client = !WebKitLinkedOnOrAfter(WEBKIT_FIRST_VERSION_WITH_3_0_CONTEXT_MENU_TAGS);
73    return preVersion3Client;
74}
75
76static BOOL isPreInspectElementTagClient(void)
77{
78    static BOOL preInspectElementTagClient = !WebKitLinkedOnOrAfter(WEBKIT_FIRST_VERSION_WITH_INSPECT_ELEMENT_MENU_TAG);
79    return preInspectElementTagClient;
80}
81
82static NSMutableArray *fixMenusToSendToOldClients(NSMutableArray *defaultMenuItems)
83{
84    NSMutableArray *savedItems = nil;
85
86    unsigned defaultItemsCount = [defaultMenuItems count];
87
88    if (isPreInspectElementTagClient() && defaultItemsCount >= 2) {
89        NSMenuItem *secondToLastItem = [defaultMenuItems objectAtIndex:defaultItemsCount - 2];
90        NSMenuItem *lastItem = [defaultMenuItems objectAtIndex:defaultItemsCount - 1];
91
92        if ([secondToLastItem isSeparatorItem] && [lastItem tag] == WebMenuItemTagInspectElement) {
93            savedItems = [NSMutableArray arrayWithCapacity:2];
94            [savedItems addObject:secondToLastItem];
95            [savedItems addObject:lastItem];
96
97            [defaultMenuItems removeObject:secondToLastItem];
98            [defaultMenuItems removeObject:lastItem];
99            defaultItemsCount -= 2;
100        }
101    }
102
103    BOOL preVersion3Client = isPreVersion3Client();
104    if (!preVersion3Client)
105        return savedItems;
106
107    for (unsigned i = 0; i < defaultItemsCount; ++i) {
108        NSMenuItem *item = [defaultMenuItems objectAtIndex:i];
109        int tag = [item tag];
110        int oldStyleTag = tag;
111
112        if (tag >= WEBMENUITEMTAG_WEBKIT_3_0_SPI_START) {
113            // Change all editing-related SPI tags listed in WebUIDelegatePrivate.h to WebMenuItemTagOther
114            // to match our old WebKit context menu behavior.
115            oldStyleTag = WebMenuItemTagOther;
116        } else {
117            // All items are expected to have useful tags coming into this method.
118            ASSERT(tag != WebMenuItemTagOther);
119
120            // Use the pre-3.0 tags for the few items that changed tags as they moved from SPI to API. We
121            // do this only for old clients; new Mail already expects the new symbols in this case.
122            if (preVersion3Client) {
123                switch (tag) {
124                    case WebMenuItemTagSearchInSpotlight:
125                        oldStyleTag = OldWebMenuItemTagSearchInSpotlight;
126                        break;
127                    case WebMenuItemTagSearchWeb:
128                        oldStyleTag = OldWebMenuItemTagSearchWeb;
129                        break;
130                    case WebMenuItemTagLookUpInDictionary:
131                        oldStyleTag = OldWebMenuItemTagLookUpInDictionary;
132                        break;
133                    default:
134                        break;
135                }
136            }
137        }
138
139        if (oldStyleTag != tag)
140            [item setTag:oldStyleTag];
141    }
142
143    return savedItems;
144}
145
146static void fixMenusReceivedFromOldClients(NSMutableArray *newMenuItems, NSMutableArray *savedItems)
147{
148    if (savedItems)
149        [newMenuItems addObjectsFromArray:savedItems];
150
151    BOOL preVersion3Client = isPreVersion3Client();
152    if (!preVersion3Client)
153        return;
154
155    // Restore the modern tags to the menu items whose tags we altered in fixMenusToSendToOldClients.
156    unsigned newItemsCount = [newMenuItems count];
157    for (unsigned i = 0; i < newItemsCount; ++i) {
158        NSMenuItem *item = [newMenuItems objectAtIndex:i];
159
160        int tag = [item tag];
161        int modernTag = tag;
162
163        if (tag == WebMenuItemTagOther) {
164            // Restore the specific tag for items on which we temporarily set WebMenuItemTagOther to match old behavior.
165            NSString *title = [item title];
166            if ([title isEqualToString:contextMenuItemTagOpenLink()])
167                modernTag = WebMenuItemTagOpenLink;
168            else if ([title isEqualToString:contextMenuItemTagIgnoreGrammar()])
169                modernTag = WebMenuItemTagIgnoreGrammar;
170            else if ([title isEqualToString:contextMenuItemTagSpellingMenu()])
171                modernTag = WebMenuItemTagSpellingMenu;
172            else if ([title isEqualToString:contextMenuItemTagShowSpellingPanel(true)]
173                     || [title isEqualToString:contextMenuItemTagShowSpellingPanel(false)])
174                modernTag = WebMenuItemTagShowSpellingPanel;
175            else if ([title isEqualToString:contextMenuItemTagCheckSpelling()])
176                modernTag = WebMenuItemTagCheckSpelling;
177            else if ([title isEqualToString:contextMenuItemTagCheckSpellingWhileTyping()])
178                modernTag = WebMenuItemTagCheckSpellingWhileTyping;
179            else if ([title isEqualToString:contextMenuItemTagCheckGrammarWithSpelling()])
180                modernTag = WebMenuItemTagCheckGrammarWithSpelling;
181            else if ([title isEqualToString:contextMenuItemTagFontMenu()])
182                modernTag = WebMenuItemTagFontMenu;
183            else if ([title isEqualToString:contextMenuItemTagShowFonts()])
184                modernTag = WebMenuItemTagShowFonts;
185            else if ([title isEqualToString:contextMenuItemTagBold()])
186                modernTag = WebMenuItemTagBold;
187            else if ([title isEqualToString:contextMenuItemTagItalic()])
188                modernTag = WebMenuItemTagItalic;
189            else if ([title isEqualToString:contextMenuItemTagUnderline()])
190                modernTag = WebMenuItemTagUnderline;
191            else if ([title isEqualToString:contextMenuItemTagOutline()])
192                modernTag = WebMenuItemTagOutline;
193            else if ([title isEqualToString:contextMenuItemTagStyles()])
194                modernTag = WebMenuItemTagStyles;
195            else if ([title isEqualToString:contextMenuItemTagShowColors()])
196                modernTag = WebMenuItemTagShowColors;
197            else if ([title isEqualToString:contextMenuItemTagSpeechMenu()])
198                modernTag = WebMenuItemTagSpeechMenu;
199            else if ([title isEqualToString:contextMenuItemTagStartSpeaking()])
200                modernTag = WebMenuItemTagStartSpeaking;
201            else if ([title isEqualToString:contextMenuItemTagStopSpeaking()])
202                modernTag = WebMenuItemTagStopSpeaking;
203            else if ([title isEqualToString:contextMenuItemTagWritingDirectionMenu()])
204                modernTag = WebMenuItemTagWritingDirectionMenu;
205            else if ([title isEqualToString:contextMenuItemTagDefaultDirection()])
206                modernTag = WebMenuItemTagDefaultDirection;
207            else if ([title isEqualToString:contextMenuItemTagLeftToRight()])
208                modernTag = WebMenuItemTagLeftToRight;
209            else if ([title isEqualToString:contextMenuItemTagRightToLeft()])
210                modernTag = WebMenuItemTagRightToLeft;
211            else if ([title isEqualToString:contextMenuItemTagInspectElement()])
212                modernTag = WebMenuItemTagInspectElement;
213            else if ([title isEqualToString:contextMenuItemTagCorrectSpellingAutomatically()])
214                modernTag = WebMenuItemTagCorrectSpellingAutomatically;
215            else if ([title isEqualToString:contextMenuItemTagSubstitutionsMenu()])
216                modernTag = WebMenuItemTagSubstitutionsMenu;
217            else if ([title isEqualToString:contextMenuItemTagShowSubstitutions(true)]
218                     || [title isEqualToString:contextMenuItemTagShowSubstitutions(false)])
219                modernTag = WebMenuItemTagShowSubstitutions;
220            else if ([title isEqualToString:contextMenuItemTagSmartCopyPaste()])
221                modernTag = WebMenuItemTagSmartCopyPaste;
222            else if ([title isEqualToString:contextMenuItemTagSmartQuotes()])
223                modernTag = WebMenuItemTagSmartQuotes;
224            else if ([title isEqualToString:contextMenuItemTagSmartDashes()])
225                modernTag = WebMenuItemTagSmartDashes;
226            else if ([title isEqualToString:contextMenuItemTagSmartLinks()])
227                modernTag = WebMenuItemTagSmartLinks;
228            else if ([title isEqualToString:contextMenuItemTagTextReplacement()])
229                modernTag = WebMenuItemTagTextReplacement;
230            else if ([title isEqualToString:contextMenuItemTagTransformationsMenu()])
231                modernTag = WebMenuItemTagTransformationsMenu;
232            else if ([title isEqualToString:contextMenuItemTagMakeUpperCase()])
233                modernTag = WebMenuItemTagMakeUpperCase;
234            else if ([title isEqualToString:contextMenuItemTagMakeLowerCase()])
235                modernTag = WebMenuItemTagMakeLowerCase;
236            else if ([title isEqualToString:contextMenuItemTagCapitalize()])
237                modernTag = WebMenuItemTagCapitalize;
238            else {
239            // We don't expect WebMenuItemTagOther for any items other than the ones we explicitly handle.
240            // There's nothing to prevent an app from applying this tag, but they are supposed to only
241            // use tags in the range starting with WebMenuItemBaseApplicationTag=10000
242                ASSERT_NOT_REACHED();
243            }
244        } else if (preVersion3Client) {
245            // Restore the new API tag for items on which we temporarily set the old SPI tag. The old SPI tag was
246            // needed to avoid confusing clients linked against earlier WebKits; the new API tag is needed for
247            // WebCore to handle the menu items appropriately (without needing to know about the old SPI tags).
248            switch (tag) {
249                case OldWebMenuItemTagSearchInSpotlight:
250                    modernTag = WebMenuItemTagSearchInSpotlight;
251                    break;
252                case OldWebMenuItemTagSearchWeb:
253                    modernTag = WebMenuItemTagSearchWeb;
254                    break;
255                case OldWebMenuItemTagLookUpInDictionary:
256                    modernTag = WebMenuItemTagLookUpInDictionary;
257                    break;
258                default:
259                    break;
260            }
261        }
262
263        if (modernTag != tag)
264            [item setTag:modernTag];
265    }
266}
267
268NSMutableArray* WebContextMenuClient::getCustomMenuFromDefaultItems(ContextMenu* defaultMenu)
269{
270    id delegate = [m_webView UIDelegate];
271    SEL selector = @selector(webView:contextMenuItemsForElement:defaultMenuItems:);
272    if (![delegate respondsToSelector:selector])
273        return defaultMenu->platformDescription();
274
275    NSDictionary *element = [[[WebElementDictionary alloc] initWithHitTestResult:[m_webView page]->contextMenuController()->hitTestResult()] autorelease];
276
277    BOOL preVersion3Client = isPreVersion3Client();
278    if (preVersion3Client) {
279        DOMNode *node = [element objectForKey:WebElementDOMNodeKey];
280        if ([node isKindOfClass:[DOMHTMLInputElement class]] && [(DOMHTMLInputElement *)node _isTextField])
281            return defaultMenu->platformDescription();
282        if ([node isKindOfClass:[DOMHTMLTextAreaElement class]])
283            return defaultMenu->platformDescription();
284    }
285
286    NSMutableArray *defaultMenuItems = defaultMenu->platformDescription();
287
288    unsigned defaultItemsCount = [defaultMenuItems count];
289    for (unsigned i = 0; i < defaultItemsCount; ++i)
290        [[defaultMenuItems objectAtIndex:i] setRepresentedObject:element];
291
292    NSMutableArray *savedItems = [fixMenusToSendToOldClients(defaultMenuItems) retain];
293    NSArray *delegateSuppliedItems = CallUIDelegate(m_webView, selector, element, defaultMenuItems);
294    NSMutableArray *newMenuItems = [delegateSuppliedItems mutableCopy];
295    fixMenusReceivedFromOldClients(newMenuItems, savedItems);
296    [savedItems release];
297    return [newMenuItems autorelease];
298}
299
300void WebContextMenuClient::contextMenuItemSelected(ContextMenuItem* item, const ContextMenu* parentMenu)
301{
302    id delegate = [m_webView UIDelegate];
303    SEL selector = @selector(webView:contextMenuItemSelected:forElement:);
304    if ([delegate respondsToSelector:selector]) {
305        NSDictionary *element = [[WebElementDictionary alloc] initWithHitTestResult:[m_webView page]->contextMenuController()->hitTestResult()];
306        NSMenuItem *platformItem = item->releasePlatformDescription();
307
308        CallUIDelegate(m_webView, selector, platformItem, element);
309
310        [element release];
311        [platformItem release];
312    }
313}
314
315void WebContextMenuClient::downloadURL(const KURL& url)
316{
317    [m_webView _downloadURL:url];
318}
319
320void WebContextMenuClient::searchWithSpotlight()
321{
322    [m_webView _searchWithSpotlightFromMenu:nil];
323}
324
325void WebContextMenuClient::searchWithGoogle(const Frame*)
326{
327    [m_webView _searchWithGoogleFromMenu:nil];
328}
329
330void WebContextMenuClient::lookUpInDictionary(Frame* frame)
331{
332    WebHTMLView* htmlView = (WebHTMLView*)[[kit(frame) frameView] documentView];
333    if(![htmlView isKindOfClass:[WebHTMLView class]])
334        return;
335    [htmlView _lookUpInDictionaryFromMenu:nil];
336}
337
338bool WebContextMenuClient::isSpeaking()
339{
340    return [NSApp isSpeaking];
341}
342
343void WebContextMenuClient::speak(const String& string)
344{
345    [NSApp speakString:[[(NSString*)string copy] autorelease]];
346}
347
348void WebContextMenuClient::stopSpeaking()
349{
350    [NSApp stopSpeaking:nil];
351}
352
353void WebContextMenuClient::showContextMenu()
354{
355    Page* page = [m_webView page];
356    if (!page)
357        return;
358    ContextMenuController* controller = page->contextMenuController();
359    Frame* frame = controller->hitTestResult().innerNodeFrame();
360    if (!frame)
361        return;
362    FrameView* frameView = frame->view();
363    if (!frameView)
364        return;
365
366    IntPoint point = frameView->contentsToWindow(controller->hitTestResult().roundedPointInInnerNodeFrame());
367    NSView* view = frameView->documentView();
368    NSPoint nsScreenPoint = [view convertPoint:point toView:nil];
369    // Show the contextual menu for this event.
370    NSEvent* event = [NSEvent mouseEventWithType:NSRightMouseDown location:nsScreenPoint modifierFlags:0 timestamp:0 windowNumber:[[view window] windowNumber] context:0 eventNumber:0 clickCount:1 pressure:1];
371    NSMenu* nsMenu = [view menuForEvent:event];
372    if (nsMenu)
373        [NSMenu popUpContextMenu:nsMenu withEvent:event forView:view];
374}
375