1/*
2 * Copyright (C) 2010 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 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#import "config.h"
27#import "WebContextMenuProxyMac.h"
28
29#if PLATFORM(MAC)
30
31#import "DataReference.h"
32#import "MenuUtilities.h"
33#import "PageClientImpl.h"
34#import "ServicesController.h"
35#import "ShareableBitmap.h"
36#import "StringUtilities.h"
37#import "WebContext.h"
38#import "WebContextMenuItemData.h"
39#import "WebProcessProxy.h"
40#import "WKView.h"
41#import <WebCore/GraphicsContext.h>
42#import <WebCore/IntRect.h>
43#import <WebKitSystemInterface.h>
44#import <wtf/RetainPtr.h>
45
46#if ENABLE(SERVICE_CONTROLS)
47#import <AppKit/NSSharingService.h>
48
49#if __has_include(<AppKit/NSSharingService_Private.h>)
50#import <AppKit/NSSharingService_Private.h>
51#else
52typedef enum {
53    NSSharingServicePickerStyleMenu = 0,
54    NSSharingServicePickerStyleRollover = 1,
55    NSSharingServicePickerStyleTextSelection = 2
56} NSSharingServicePickerStyle;
57#endif
58
59@interface NSSharingServicePicker (Details)
60@property NSSharingServicePickerStyle style;
61- (NSMenu *)menu;
62@end
63
64#endif // ENABLE(SERVICE_CONTROLS)
65
66using namespace WebCore;
67
68@interface WKUserDataWrapper : NSObject {
69    RefPtr<API::Object> _webUserData;
70}
71- (id)initWithUserData:(API::Object*)userData;
72- (API::Object*)userData;
73@end
74
75@implementation WKUserDataWrapper
76
77- (id)initWithUserData:(API::Object*)userData
78{
79    self = [super init];
80    if (!self)
81        return nil;
82
83    _webUserData = userData;
84    return self;
85}
86
87- (API::Object*)userData
88{
89    return _webUserData.get();
90}
91
92@end
93
94@interface WKSelectionHandlerWrapper : NSObject {
95    std::function<void()> _selectionHandler;
96}
97- (id)initWithSelectionHandler:(std::function<void()>)selectionHandler;
98- (void)executeSelectionHandler;
99@end
100
101@implementation WKSelectionHandlerWrapper
102- (id)initWithSelectionHandler:(std::function<void()>)selectionHandler
103{
104    self = [super init];
105    if (!self)
106        return nil;
107
108    _selectionHandler = selectionHandler;
109    return self;
110}
111
112- (void)executeSelectionHandler
113{
114    if (_selectionHandler)
115        _selectionHandler();
116}
117@end
118
119@interface WKMenuTarget : NSObject {
120    WebKit::WebContextMenuProxyMac* _menuProxy;
121}
122+ (WKMenuTarget *)sharedMenuTarget;
123- (WebKit::WebContextMenuProxyMac*)menuProxy;
124- (void)setMenuProxy:(WebKit::WebContextMenuProxyMac*)menuProxy;
125- (void)forwardContextMenuAction:(id)sender;
126@end
127
128@implementation WKMenuTarget
129
130+ (WKMenuTarget*)sharedMenuTarget
131{
132    static WKMenuTarget* target = [[WKMenuTarget alloc] init];
133    return target;
134}
135
136- (WebKit::WebContextMenuProxyMac*)menuProxy
137{
138    return _menuProxy;
139}
140
141- (void)setMenuProxy:(WebKit::WebContextMenuProxyMac*)menuProxy
142{
143    _menuProxy = menuProxy;
144}
145
146- (void)forwardContextMenuAction:(id)sender
147{
148    id representedObject = [sender representedObject];
149
150    // NSMenuItems with a represented selection handler belong solely to the UI process
151    // and don't need any further processing after the selection handler is called.
152    if ([representedObject isKindOfClass:[WKSelectionHandlerWrapper class]]) {
153        [representedObject executeSelectionHandler];
154        return;
155    }
156
157    WebKit::WebContextMenuItemData item(ActionType, static_cast<ContextMenuAction>([sender tag]), [sender title], [sender isEnabled], [sender state] == NSOnState);
158    if (representedObject) {
159        ASSERT([representedObject isKindOfClass:[WKUserDataWrapper class]]);
160        item.setUserData([static_cast<WKUserDataWrapper *>(representedObject) userData]);
161    }
162
163    _menuProxy->contextMenuItemSelected(item);
164}
165
166@end
167
168#if ENABLE(SERVICE_CONTROLS)
169@interface WKSharingServicePickerDelegate : NSObject <NSSharingServiceDelegate, NSSharingServicePickerDelegate> {
170    WebKit::WebContextMenuProxyMac* _menuProxy;
171    RetainPtr<NSSharingServicePicker> _picker;
172    BOOL _includeEditorServices;
173}
174
175+ (WKSharingServicePickerDelegate *)sharedSharingServicePickerDelegate;
176- (WebKit::WebContextMenuProxyMac*)menuProxy;
177- (void)setMenuProxy:(WebKit::WebContextMenuProxyMac*)menuProxy;
178- (void)setPicker:(NSSharingServicePicker *)picker;
179- (void)setIncludeEditorServices:(BOOL)includeEditorServices;
180@end
181
182// FIXME: We probably need to hang on the picker itself until the context menu operation is done, and this object will probably do that.
183@implementation WKSharingServicePickerDelegate
184+ (WKSharingServicePickerDelegate*)sharedSharingServicePickerDelegate
185{
186    static WKSharingServicePickerDelegate* delegate = [[WKSharingServicePickerDelegate alloc] init];
187    return delegate;
188}
189
190- (WebKit::WebContextMenuProxyMac*)menuProxy
191{
192    return _menuProxy;
193}
194
195- (void)setMenuProxy:(WebKit::WebContextMenuProxyMac*)menuProxy
196{
197    _menuProxy = menuProxy;
198}
199
200- (void)setPicker:(NSSharingServicePicker *)picker
201{
202    _picker = picker;
203}
204
205- (void)setIncludeEditorServices:(BOOL)includeEditorServices
206{
207    _includeEditorServices = includeEditorServices;
208}
209
210- (NSArray *)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker sharingServicesForItems:(NSArray *)items mask:(NSSharingServiceMask)mask proposedSharingServices:(NSArray *)proposedServices
211{
212    if (_includeEditorServices)
213        return proposedServices;
214
215    NSMutableArray *services = [[NSMutableArray alloc] initWithCapacity:[proposedServices count]];
216
217    for (NSSharingService *service in proposedServices) {
218        if (service.type != NSSharingServiceTypeEditor)
219            [services addObject:service];
220    }
221
222    return services;
223}
224
225- (id <NSSharingServiceDelegate>)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker delegateForSharingService:(NSSharingService *)sharingService
226{
227    return self;
228}
229
230- (void)sharingService:(NSSharingService *)sharingService willShareItems:(NSArray *)items
231{
232    _menuProxy->clearServicesMenu();
233}
234
235- (void)sharingService:(NSSharingService *)sharingService didShareItems:(NSArray *)items
236{
237    // We only care about what item was shared if we were interested in editor services
238    // (i.e., if we plan on replacing the selection with the returned item)
239    if (!_includeEditorServices)
240        return;
241
242    Vector<String> types;
243    IPC::DataReference dataReference;
244
245    id item = [items objectAtIndex:0];
246
247    if ([item isKindOfClass:[NSAttributedString class]]) {
248        NSData *data = [item RTFDFromRange:NSMakeRange(0, [item length]) documentAttributes:nil];
249        dataReference = IPC::DataReference(static_cast<const uint8_t*>([data bytes]), [data length]);
250
251        types.append(NSPasteboardTypeRTFD);
252        types.append(NSRTFDPboardType);
253    } else if ([item isKindOfClass:[NSData class]]) {
254        NSData *data = (NSData *)item;
255        RetainPtr<CGImageSourceRef> source = adoptCF(CGImageSourceCreateWithData((CFDataRef)data, NULL));
256        RetainPtr<CGImageRef> image = adoptCF(CGImageSourceCreateImageAtIndex(source.get(), 0, NULL));
257
258        if (!image)
259            return;
260
261        dataReference = IPC::DataReference(static_cast<const uint8_t*>([data bytes]), [data length]);
262        types.append(NSPasteboardTypeTIFF);
263    } else {
264        LOG_ERROR("sharingService:didShareItems: - Unknown item type returned\n");
265        return;
266    }
267
268    _menuProxy->page().replaceSelectionWithPasteboardData(types, dataReference);
269}
270
271- (NSWindow *)sharingService:(NSSharingService *)sharingService sourceWindowForShareItems:(NSArray *)items sharingContentScope:(NSSharingContentScope *)sharingContentScope
272{
273    return _menuProxy->window();
274}
275
276@end
277
278#endif
279
280namespace WebKit {
281
282WebContextMenuProxyMac::WebContextMenuProxyMac(WKView* webView, WebPageProxy* page)
283    : m_webView(webView)
284    , m_page(page)
285{
286    ASSERT(m_page);
287}
288
289WebContextMenuProxyMac::~WebContextMenuProxyMac()
290{
291    if (m_popup)
292        [m_popup setControlView:nil];
293}
294
295void WebContextMenuProxyMac::contextMenuItemSelected(const WebContextMenuItemData& item)
296{
297#if ENABLE(SERVICE_CONTROLS)
298    clearServicesMenu();
299#endif
300
301    m_page->contextMenuItemSelected(item);
302}
303
304static void populateNSMenu(NSMenu* menu, const Vector<RetainPtr<NSMenuItem>>& menuItemVector)
305{
306    for (unsigned i = 0; i < menuItemVector.size(); ++i) {
307        NSInteger oldState = [menuItemVector[i].get() state];
308        [menu addItem:menuItemVector[i].get()];
309        [menuItemVector[i].get() setState:oldState];
310    }
311}
312
313static Vector<RetainPtr<NSMenuItem>> nsMenuItemVector(const Vector<WebContextMenuItemData>& items)
314{
315    Vector<RetainPtr<NSMenuItem>> result;
316
317    unsigned size = items.size();
318    result.reserveCapacity(size);
319    for (unsigned i = 0; i < size; i++) {
320        switch (items[i].type()) {
321        case ActionType:
322        case CheckableActionType: {
323            NSMenuItem* menuItem = [[NSMenuItem alloc] initWithTitle:nsStringFromWebCoreString(items[i].title()) action:@selector(forwardContextMenuAction:) keyEquivalent:@""];
324            [menuItem setTag:items[i].action()];
325            [menuItem setEnabled:items[i].enabled()];
326            [menuItem setState:items[i].checked() ? NSOnState : NSOffState];
327
328            if (std::function<void()> selectionHandler = items[i].selectionHandler()) {
329                WKSelectionHandlerWrapper *wrapper = [[WKSelectionHandlerWrapper alloc] initWithSelectionHandler:selectionHandler];
330                [menuItem setRepresentedObject:wrapper];
331                [wrapper release];
332            } else if (items[i].userData()) {
333                WKUserDataWrapper *wrapper = [[WKUserDataWrapper alloc] initWithUserData:items[i].userData()];
334                [menuItem setRepresentedObject:wrapper];
335                [wrapper release];
336            }
337
338            result.append(adoptNS(menuItem));
339            break;
340        }
341        case SeparatorType:
342            result.append([NSMenuItem separatorItem]);
343            break;
344        case SubmenuType: {
345            NSMenu* menu = [[NSMenu alloc] initWithTitle:nsStringFromWebCoreString(items[i].title())];
346            [menu setAutoenablesItems:NO];
347            populateNSMenu(menu, nsMenuItemVector(items[i].submenu()));
348
349            NSMenuItem* menuItem = [[NSMenuItem alloc] initWithTitle:nsStringFromWebCoreString(items[i].title()) action:@selector(forwardContextMenuAction:) keyEquivalent:@""];
350            [menuItem setEnabled:items[i].enabled()];
351            [menuItem setSubmenu:menu];
352            [menu release];
353
354            result.append(adoptNS(menuItem));
355
356            break;
357        }
358        default:
359            ASSERT_NOT_REACHED();
360        }
361    }
362
363    WKMenuTarget* target = [WKMenuTarget sharedMenuTarget];
364    for (unsigned i = 0; i < size; ++i)
365        [result[i].get() setTarget:target];
366
367    return result;
368}
369
370#if ENABLE(SERVICE_CONTROLS)
371
372void WebContextMenuProxyMac::setupServicesMenu(const ContextMenuContextData& context)
373{
374    bool includeEditorServices = context.controlledDataIsEditable();
375    bool hasControlledImage = !context.controlledImageHandle().isNull();
376    NSArray *items = nil;
377    if (hasControlledImage) {
378        RefPtr<ShareableBitmap> image = ShareableBitmap::create(context.controlledImageHandle());
379        if (!image)
380            return;
381
382        RetainPtr<CGImageRef> cgImage = image->makeCGImage();
383        RetainPtr<NSImage> nsImage = adoptNS([[NSImage alloc] initWithCGImage:cgImage.get() size:image->size()]);
384        items = @[ nsImage.get() ];
385    } else if (!context.controlledSelectionData().isEmpty()) {
386        RetainPtr<NSData> selectionData = adoptNS([[NSData alloc] initWithBytes:(void*)context.controlledSelectionData().data() length:context.controlledSelectionData().size()]);
387        RetainPtr<NSAttributedString> selection = adoptNS([[NSAttributedString alloc] initWithRTFD:selectionData.get() documentAttributes:nil]);
388
389        items = @[ selection.get() ];
390    } else {
391        LOG_ERROR("No service controlled item represented in the context");
392        return;
393    }
394
395    RetainPtr<NSSharingServicePicker> picker = adoptNS([[NSSharingServicePicker alloc] initWithItems:items]);
396    [picker setStyle:hasControlledImage ? NSSharingServicePickerStyleRollover : NSSharingServicePickerStyleTextSelection];
397    [picker setDelegate:[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate]];
398    [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setPicker:picker.get()];
399    [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setIncludeEditorServices:includeEditorServices];
400
401    m_servicesMenu = adoptNS([[picker menu] copy]);
402
403    if (!hasControlledImage)
404        [m_servicesMenu setShowsStateColumn:YES];
405
406    // Explicitly add a menu item for each telephone number that is in the selection.
407    const Vector<String>& selectedTelephoneNumbers = context.selectedTelephoneNumbers();
408    Vector<RetainPtr<NSMenuItem>> telephoneNumberMenuItems;
409    for (auto& telephoneNumber : selectedTelephoneNumbers) {
410        if (NSMenuItem *item = menuItemForTelephoneNumber(telephoneNumber)) {
411            [item setIndentationLevel:1];
412            telephoneNumberMenuItems.append(item);
413        }
414    }
415
416    if (!telephoneNumberMenuItems.isEmpty()) {
417        if (m_servicesMenu)
418            [m_servicesMenu insertItem:[NSMenuItem separatorItem] atIndex:0];
419        else
420            m_servicesMenu = adoptNS([[NSMenu alloc] init]);
421        int itemPosition = 0;
422        NSMenuItem *groupEntry = [[NSMenuItem alloc] initWithTitle:menuItemTitleForTelephoneNumberGroup() action:nil keyEquivalent:@""];
423        [groupEntry setEnabled:NO];
424        [m_servicesMenu insertItem:groupEntry atIndex:itemPosition++];
425        for (auto& menuItem : telephoneNumberMenuItems)
426            [m_servicesMenu insertItem:menuItem.get() atIndex:itemPosition++];
427    }
428
429    // If there is no services menu, then the existing services on the system have changed, so refresh that list of services.
430    // If <rdar://problem/17954709> is resolved then we can more accurately keep the list up to date without this call.
431    if (!m_servicesMenu)
432        ServicesController::shared().refreshExistingServices();
433}
434
435void WebContextMenuProxyMac::clearServicesMenu()
436{
437    [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setPicker:nullptr];
438    m_servicesMenu = nullptr;
439}
440#endif
441
442void WebContextMenuProxyMac::populate(const Vector<WebContextMenuItemData>& items, const ContextMenuContextData& context)
443{
444#if ENABLE(SERVICE_CONTROLS)
445    if (context.needsServicesMenu()) {
446        setupServicesMenu(context);
447        return;
448    }
449#endif
450
451    if (m_popup)
452        [m_popup removeAllItems];
453    else {
454        m_popup = adoptNS([[NSPopUpButtonCell alloc] initTextCell:@"" pullsDown:NO]);
455        [m_popup setUsesItemFromMenu:NO];
456        [m_popup setAutoenablesItems:NO];
457    }
458
459    NSMenu* menu = [m_popup menu];
460    populateNSMenu(menu, nsMenuItemVector(items));
461}
462
463void WebContextMenuProxyMac::showContextMenu(const IntPoint& menuLocation, const Vector<WebContextMenuItemData>& items, const ContextMenuContextData& context)
464{
465#if ENABLE(SERVICE_CONTROLS)
466    if (items.isEmpty() && !context.needsServicesMenu())
467        return;
468#else
469    if (items.isEmpty())
470        return;
471#endif
472
473    populate(items, context);
474
475    [[WKMenuTarget sharedMenuTarget] setMenuProxy:this];
476
477    NSRect menuRect = NSMakeRect(menuLocation.x(), menuLocation.y(), 0, 0);
478
479#if ENABLE(SERVICE_CONTROLS)
480    if (context.needsServicesMenu())
481        [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setMenuProxy:this];
482
483    if (!m_servicesMenu)
484        [m_popup attachPopUpWithFrame:menuRect inView:m_webView];
485
486    NSMenu *menu = m_servicesMenu ? m_servicesMenu.get() : [m_popup menu];
487
488    // Telephone number and service menus must use the [NSMenu popUpMenuPositioningItem:atLocation:inView:] API.
489    // FIXME: That API is better than WKPopupContextMenu. In the future all menus should use either it
490    // or the [NSMenu popUpContextMenu:withEvent:forView:] API, depending on the menu type.
491    // Then we could get rid of NSPopUpButtonCell, custom metrics, and WKPopupContextMenu.
492    if (context.isTelephoneNumberContext() || context.needsServicesMenu()) {
493        [menu popUpMenuPositioningItem:nil atLocation:menuLocation inView:m_webView];
494        hideContextMenu();
495        return;
496    }
497
498#else
499    [m_popup attachPopUpWithFrame:menuRect inView:m_webView];
500
501    NSMenu *menu = [m_popup menu];
502#endif
503
504    // These values were borrowed from AppKit to match their placement of the menu.
505    NSRect titleFrame = [m_popup titleRectForBounds:menuRect];
506    if (titleFrame.size.width <= 0 || titleFrame.size.height <= 0)
507        titleFrame = menuRect;
508    float vertOffset = roundf((NSMaxY(menuRect) - NSMaxY(titleFrame)) + NSHeight(titleFrame));
509    NSPoint location = NSMakePoint(NSMinX(menuRect), NSMaxY(menuRect) - vertOffset);
510
511    location = [m_webView convertPoint:location toView:nil];
512#pragma clang diagnostic push
513#pragma clang diagnostic ignored "-Wdeprecated-declarations"
514    location = [m_webView.window convertBaseToScreen:location];
515#pragma clang diagnostic pop
516
517    WKPopupContextMenu(menu, location);
518
519    hideContextMenu();
520}
521
522void WebContextMenuProxyMac::hideContextMenu()
523{
524    [m_popup dismissPopUp];
525}
526
527NSWindow *WebContextMenuProxyMac::window() const
528{
529    return [m_webView window];
530}
531
532} // namespace WebKit
533
534#endif // PLATFORM(MAC)
535