Add a context menu delegate method that takes a userInfo parameter
[WebKit-https.git] / Source / WebKit2 / UIProcess / mac / WebContextMenuProxyMac.mm
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 "APIContextMenuClient.h"
32 #import "DataReference.h"
33 #import "MenuUtilities.h"
34 #import "PageClientImpl.h"
35 #import "ServicesController.h"
36 #import "ShareableBitmap.h"
37 #import "StringUtilities.h"
38 #import "WKSharingServicePickerDelegate.h"
39 #import "WebContextMenuItem.h"
40 #import "WebContextMenuItemData.h"
41 #import "WebProcessProxy.h"
42 #import <WebCore/GraphicsContext.h>
43 #import <WebCore/IntRect.h>
44 #import <WebCore/NSMenuSPI.h>
45 #import <WebCore/NSSharingServicePickerSPI.h>
46 #import <WebCore/NSSharingServiceSPI.h>
47 #import <WebKitSystemInterface.h>
48 #import <wtf/RetainPtr.h>
49
50 using namespace WebCore;
51
52 @interface WKUserDataWrapper : NSObject {
53     RefPtr<API::Object> _webUserData;
54 }
55 - (id)initWithUserData:(API::Object*)userData;
56 - (API::Object*)userData;
57 @end
58
59 @implementation WKUserDataWrapper
60
61 - (id)initWithUserData:(API::Object*)userData
62 {
63     self = [super init];
64     if (!self)
65         return nil;
66     
67     _webUserData = userData;
68     return self;
69 }
70
71 - (API::Object*)userData
72 {
73     return _webUserData.get();
74 }
75
76 @end
77
78 @interface WKSelectionHandlerWrapper : NSObject {
79     std::function<void ()> _selectionHandler;
80 }
81 - (id)initWithSelectionHandler:(std::function<void ()>)selectionHandler;
82 - (void)executeSelectionHandler;
83 @end
84
85 @implementation WKSelectionHandlerWrapper
86 - (id)initWithSelectionHandler:(std::function<void ()>)selectionHandler
87 {
88     self = [super init];
89     if (!self)
90         return nil;
91     
92     _selectionHandler = selectionHandler;
93     return self;
94 }
95
96 - (void)executeSelectionHandler
97 {
98     if (_selectionHandler)
99         _selectionHandler();
100 }
101 @end
102
103 @interface WKMenuTarget : NSObject {
104     WebKit::WebContextMenuProxyMac* _menuProxy;
105 }
106 + (WKMenuTarget *)sharedMenuTarget;
107 - (WebKit::WebContextMenuProxyMac*)menuProxy;
108 - (void)setMenuProxy:(WebKit::WebContextMenuProxyMac*)menuProxy;
109 - (void)forwardContextMenuAction:(id)sender;
110 @end
111
112 @implementation WKMenuTarget
113
114 + (WKMenuTarget*)sharedMenuTarget
115 {
116     static WKMenuTarget* target = [[WKMenuTarget alloc] init];
117     return target;
118 }
119
120 - (WebKit::WebContextMenuProxyMac*)menuProxy
121 {
122     return _menuProxy;
123 }
124
125 - (void)setMenuProxy:(WebKit::WebContextMenuProxyMac*)menuProxy
126 {
127     _menuProxy = menuProxy;
128 }
129
130 - (void)forwardContextMenuAction:(id)sender
131 {
132     id representedObject = [sender representedObject];
133
134     // NSMenuItems with a represented selection handler belong solely to the UI process
135     // and don't need any further processing after the selection handler is called.
136     if ([representedObject isKindOfClass:[WKSelectionHandlerWrapper class]]) {
137         [representedObject executeSelectionHandler];
138         return;
139     }
140
141     WebKit::WebContextMenuItemData item(ActionType, static_cast<ContextMenuAction>([sender tag]), [sender title], [sender isEnabled], [sender state] == NSOnState);
142     if (representedObject) {
143         ASSERT([representedObject isKindOfClass:[WKUserDataWrapper class]]);
144         item.setUserData([static_cast<WKUserDataWrapper *>(representedObject) userData]);
145     }
146
147     _menuProxy->contextMenuItemSelected(item);
148 }
149
150 @end
151
152 namespace WebKit {
153
154 WebContextMenuProxyMac::WebContextMenuProxyMac(NSView* webView, WebPageProxy& page, const ContextMenuContextData& context, const UserData& userData)
155     : WebContextMenuProxy(context, userData)
156     , m_webView(webView)
157     , m_page(page)
158 {
159 }
160
161 WebContextMenuProxyMac::~WebContextMenuProxyMac()
162 {
163     [m_menu cancelTracking];
164 }
165
166 void WebContextMenuProxyMac::contextMenuItemSelected(const WebContextMenuItemData& item)
167 {
168 #if ENABLE(SERVICE_CONTROLS)
169     clearServicesMenu();
170 #endif
171
172     m_page.contextMenuItemSelected(item);
173 }
174
175 #if ENABLE(SERVICE_CONTROLS)
176 void WebContextMenuProxyMac::setupServicesMenu()
177 {
178     bool includeEditorServices = m_context.controlledDataIsEditable();
179     bool hasControlledImage = m_context.controlledImage();
180     NSArray *items = nil;
181     if (hasControlledImage) {
182         RefPtr<ShareableBitmap> image = m_context.controlledImage();
183         if (!image)
184             return;
185
186         auto cgImage = image->makeCGImage();
187         auto nsImage = adoptNS([[NSImage alloc] initWithCGImage:cgImage.get() size:image->size()]);
188
189 #ifdef __LP64__
190         auto itemProvider = adoptNS([[NSItemProvider alloc] initWithItem:[nsImage TIFFRepresentation] typeIdentifier:(__bridge NSString *)kUTTypeTIFF]);
191         items = @[ itemProvider.get() ];
192 #else
193         items = @[ ];
194 #endif
195     } else if (!m_context.controlledSelectionData().isEmpty()) {
196         auto selectionData = adoptNS([[NSData alloc] initWithBytes:static_cast<const void*>(m_context.controlledSelectionData().data()) length:m_context.controlledSelectionData().size()]);
197         auto selection = adoptNS([[NSAttributedString alloc] initWithRTFD:selectionData.get() documentAttributes:nil]);
198
199         items = @[ selection.get() ];
200     } else {
201         LOG_ERROR("No service controlled item represented in the context");
202         return;
203     }
204
205     RetainPtr<NSSharingServicePicker> picker = adoptNS([[NSSharingServicePicker alloc] initWithItems:items]);
206     [picker setStyle:hasControlledImage ? NSSharingServicePickerStyleRollover : NSSharingServicePickerStyleTextSelection];
207     [picker setDelegate:[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate]];
208     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setPicker:picker.get()];
209     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setFiltersEditingServices:!includeEditorServices];
210     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setHandlesEditingReplacement:includeEditorServices];
211
212     m_menu = adoptNS([[picker menu] copy]);
213
214     if (!hasControlledImage)
215         [m_menu setShowsStateColumn:YES];
216
217     // Explicitly add a menu item for each telephone number that is in the selection.
218     Vector<RetainPtr<NSMenuItem>> telephoneNumberMenuItems;
219
220     for (auto& telephoneNumber : m_context.selectedTelephoneNumbers()) {
221         if (NSMenuItem *item = menuItemForTelephoneNumber(telephoneNumber)) {
222             [item setIndentationLevel:1];
223             telephoneNumberMenuItems.append(item);
224         }
225     }
226
227     if (!telephoneNumberMenuItems.isEmpty()) {
228         if (m_menu)
229             [m_menu insertItem:[NSMenuItem separatorItem] atIndex:0];
230         else
231             m_menu = adoptNS([[NSMenu alloc] init]);
232         int itemPosition = 0;
233         NSMenuItem *groupEntry = [[NSMenuItem alloc] initWithTitle:menuItemTitleForTelephoneNumberGroup() action:nil keyEquivalent:@""];
234         [groupEntry setEnabled:NO];
235         [m_menu insertItem:groupEntry atIndex:itemPosition++];
236         for (auto& menuItem : telephoneNumberMenuItems)
237             [m_menu insertItem:menuItem.get() atIndex:itemPosition++];
238     }
239
240     // If there is no services menu, then the existing services on the system have changed, so refresh that list of services.
241     // If <rdar://problem/17954709> is resolved then we can more accurately keep the list up to date without this call.
242     if (!m_menu)
243         ServicesController::singleton().refreshExistingServices();
244 }
245
246 void WebContextMenuProxyMac::showServicesMenu()
247 {
248     setupServicesMenu();
249
250     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setMenuProxy:this];
251     [m_menu popUpMenuPositioningItem:nil atLocation:m_context.menuLocation() inView:m_webView];
252 }
253
254 void WebContextMenuProxyMac::clearServicesMenu()
255 {
256     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setPicker:nullptr];
257     m_menu = nullptr;
258 }
259
260 RetainPtr<NSMenuItem> WebContextMenuProxyMac::createShareMenuItem()
261 {
262     if (![[NSMenuItem class] respondsToSelector:@selector(standardShareMenuItemWithItems:)])
263         return nil;
264
265     const WebHitTestResultData& hitTestData = m_context.webHitTestResultData();
266
267     auto items = adoptNS([[NSMutableArray alloc] init]);
268
269     if (!hitTestData.absoluteLinkURL.isEmpty()) {
270         NSURL *absoluteLinkURL = [NSURL URLWithString:hitTestData.absoluteLinkURL];
271         [items addObject:absoluteLinkURL];
272     }
273
274     if (hitTestData.isDownloadableMedia && !hitTestData.absoluteMediaURL.isEmpty()) {
275         NSURL *downloadableMediaURL = [NSURL URLWithString:hitTestData.absoluteMediaURL];
276         [items addObject:downloadableMediaURL];
277     }
278
279     if (hitTestData.imageSharedMemory && hitTestData.imageSize) {
280         auto image = adoptNS([[NSImage alloc] initWithData:[NSData dataWithBytes:(unsigned char*)hitTestData.imageSharedMemory->data() length:hitTestData.imageSize]]);
281         [items addObject:image.get()];
282     }
283
284     if (![items count])
285         return nil;
286
287     RetainPtr<NSMenuItem> item = [NSMenuItem standardShareMenuItemWithItems:items.get()];
288     if (!item)
289         return nil;
290
291     NSSharingServicePicker *sharingServicePicker = [item representedObject];
292     sharingServicePicker.delegate = [WKSharingServicePickerDelegate sharedSharingServicePickerDelegate];
293
294     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setFiltersEditingServices:NO];
295     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setHandlesEditingReplacement:NO];
296     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setMenuProxy:this];
297
298     // Setting the picker lets the delegate retain it to keep it alive, but this picker is kept alive by the menu item.
299     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setPicker:nil];
300
301     return item;
302 }
303 #endif
304
305 void WebContextMenuProxyMac::show()
306 {
307     Ref<WebPageProxy> protect(m_page);
308
309 #if ENABLE(SERVICE_CONTROLS)
310     if (m_context.isServicesMenu()) {
311         showServicesMenu();
312         return;
313     }
314 #endif
315
316     showContextMenu();
317 }
318
319 RetainPtr<NSMenu> WebContextMenuProxyMac::createContextMenuFromItems(const Vector<WebContextMenuItemData>& items)
320 {
321     auto menu = adoptNS([[NSMenu alloc] initWithTitle:@""]);
322     [menu setAutoenablesItems:NO];
323
324     for (auto& item : items) {
325         if (auto menuItem = createContextMenuItem(item))
326             [menu addItem:menuItem.get()];
327     }
328
329     return menu;
330 }
331
332 RetainPtr<NSMenuItem> WebContextMenuProxyMac::createContextMenuItem(const WebContextMenuItemData& item)
333 {
334 #if ENABLE(SERVICE_CONTROLS)
335     if (item.action() == ContextMenuItemTagShareMenu)
336         return createShareMenuItem();
337 #endif
338
339     switch (item.type()) {
340     case ActionType:
341     case CheckableActionType: {
342         auto menuItem = adoptNS([[NSMenuItem alloc] initWithTitle:item.title() action:@selector(forwardContextMenuAction:) keyEquivalent:@""]);
343
344         [menuItem setTag:item.action()];
345         [menuItem setEnabled:item.enabled()];
346         [menuItem setState:item.checked() ? NSOnState : NSOffState];
347         [menuItem setTarget:[WKMenuTarget sharedMenuTarget]];
348
349         if (item.userData()) {
350             auto wrapper = adoptNS([[WKUserDataWrapper alloc] initWithUserData:item.userData()]);
351             [menuItem setRepresentedObject:wrapper.get()];
352         }
353
354         return menuItem;
355     }
356
357     case SeparatorType:
358         return [NSMenuItem separatorItem];
359
360     case SubmenuType: {
361         auto menuItem = adoptNS([[NSMenuItem alloc] initWithTitle:item.title() action:nullptr keyEquivalent:@""]);
362         [menuItem setEnabled:item.enabled()];
363         [menuItem setSubmenu:createContextMenuFromItems(item.submenu()).get()];
364
365         return menuItem;
366     }
367     }
368 }
369
370 void WebContextMenuProxyMac::showContextMenu()
371 {
372     Vector<RefPtr<WebContextMenuItem>> proposedAPIItems;
373     for (auto& item : m_context.menuItems())
374         proposedAPIItems.append(WebContextMenuItem::create(item));
375
376     Vector<RefPtr<WebContextMenuItem>> clientItems;
377     bool useProposedItems = true;
378
379     // FIXME: Get rid of this once we don't need the C SPI.
380     if (m_page.contextMenuClient().getContextMenuFromProposedMenu(m_page, proposedAPIItems, clientItems, m_context.webHitTestResultData(), m_page.process().transformHandlesToObjects(m_userData.object()).get()))
381         useProposedItems = false;
382
383     Vector<WebContextMenuItemData> items;
384     for (auto& item : (useProposedItems ? proposedAPIItems : clientItems))
385         items.append(item->data());
386
387     if (items.isEmpty())
388         return;
389
390     auto menu = createContextMenuFromItems(items);
391     m_menu = m_page.contextMenuClient().menuFromProposedMenu(m_page, menu.get(), m_context.webHitTestResultData(), m_userData.object());
392
393     [[WKMenuTarget sharedMenuTarget] setMenuProxy:this];
394
395     NSPoint menuLocation = [m_webView convertPoint:m_context.menuLocation() toView:nil];
396     NSEvent *event = [NSEvent mouseEventWithType:NSRightMouseUp location:menuLocation modifierFlags:0 timestamp:0 windowNumber:m_webView.window.windowNumber context:nil eventNumber:0 clickCount:0 pressure:0];
397     [NSMenu popUpContextMenu:m_menu.get() withEvent:event forView:m_webView];
398 }
399
400 NSWindow *WebContextMenuProxyMac::window() const
401 {
402     return [m_webView window];
403 }
404
405 } // namespace WebKit
406
407 #endif // PLATFORM(MAC)