ASSERT when right clicking on SVG Image generating Share menu - can break Web Inspector
[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 "WKView.h"
40 #import "WebContextMenuItem.h"
41 #import "WebContextMenuItemData.h"
42 #import "WebProcessProxy.h"
43 #import <WebCore/GraphicsContext.h>
44 #import <WebCore/IntRect.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(WKView* 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_popup setControlView:nil];
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 static void populateNSMenu(NSMenu* menu, const Vector<RetainPtr<NSMenuItem>>& menuItemVector)
176 {
177     for (unsigned i = 0; i < menuItemVector.size(); ++i) {
178         NSInteger oldState = [menuItemVector[i].get() state];
179         [menu addItem:menuItemVector[i].get()];
180         [menuItemVector[i].get() setState:oldState];
181     }
182 }
183
184 template<typename ItemType> static Vector<RetainPtr<NSMenuItem>> nsMenuItemVector(const Vector<ItemType>&);
185
186 static RetainPtr<NSMenuItem> nsMenuItem(const WebContextMenuItemData& item)
187 {
188     switch (item.type()) {
189     case ActionType:
190     case CheckableActionType: {
191         NSMenuItem* menuItem = [[NSMenuItem alloc] initWithTitle:nsStringFromWebCoreString(item.title()) action:@selector(forwardContextMenuAction:) keyEquivalent:@""];
192         [menuItem setTag:item.action()];
193         [menuItem setEnabled:item.enabled()];
194         [menuItem setState:item.checked() ? NSOnState : NSOffState];
195
196         if (item.userData()) {
197             WKUserDataWrapper *wrapper = [[WKUserDataWrapper alloc] initWithUserData:item.userData()];
198             [menuItem setRepresentedObject:wrapper];
199             [wrapper release];
200         }
201
202         return adoptNS(menuItem);
203         break;
204     }
205     case SeparatorType:
206         return [NSMenuItem separatorItem];
207         break;
208     case SubmenuType: {
209         NSMenu* menu = [[NSMenu alloc] initWithTitle:nsStringFromWebCoreString(item.title())];
210         [menu setAutoenablesItems:NO];
211         populateNSMenu(menu, nsMenuItemVector(item.submenu()));
212             
213         NSMenuItem* menuItem = [[NSMenuItem alloc] initWithTitle:nsStringFromWebCoreString(item.title()) action:@selector(forwardContextMenuAction:) keyEquivalent:@""];
214         [menuItem setEnabled:item.enabled()];
215         [menuItem setSubmenu:menu];
216         [menu release];
217
218         return adoptNS(menuItem);
219     }
220     default:
221         ASSERT_NOT_REACHED();
222     }
223 }
224
225 static RetainPtr<NSMenuItem> nsMenuItem(const RefPtr<WebContextMenuItem>& item)
226 {
227     if (NativeContextMenuItem* nativeItem = item->nativeContextMenuItem())
228         return nativeItem->nsMenuItem();
229
230     ASSERT(item->data());
231     return nsMenuItem(*item->data());
232 }
233
234 template<typename ItemType> static Vector<RetainPtr<NSMenuItem>> nsMenuItemVector(const Vector<ItemType>& items)
235 {
236     Vector<RetainPtr<NSMenuItem>> result;
237
238     unsigned size = items.size();
239     result.reserveCapacity(size);
240     for (auto& item : items)
241         result.uncheckedAppend(nsMenuItem(item));
242
243     WKMenuTarget* target = [WKMenuTarget sharedMenuTarget];
244     for (auto& item : result)
245         [item.get() setTarget:target];
246     
247     return result;
248 }
249
250 #if ENABLE(SERVICE_CONTROLS)
251 void WebContextMenuProxyMac::setupServicesMenu(const ContextMenuContextData& context)
252 {
253     bool includeEditorServices = context.controlledDataIsEditable();
254     bool hasControlledImage = context.controlledImage();
255     NSArray *items = nil;
256     if (hasControlledImage) {
257         RefPtr<ShareableBitmap> image = context.controlledImage();
258         if (!image)
259             return;
260
261         auto cgImage = image->makeCGImage();
262         auto nsImage = adoptNS([[NSImage alloc] initWithCGImage:cgImage.get() size:image->size()]);
263
264 #ifdef __LP64__
265         auto itemProvider = adoptNS([[NSItemProvider alloc] initWithItem:[nsImage TIFFRepresentation] typeIdentifier:(__bridge NSString *)kUTTypeTIFF]);
266         items = @[ itemProvider.get() ];
267 #else
268         items = @[ ];
269 #endif
270     } else if (!context.controlledSelectionData().isEmpty()) {
271         RetainPtr<NSData> selectionData = adoptNS([[NSData alloc] initWithBytes:(void*)context.controlledSelectionData().data() length:context.controlledSelectionData().size()]);
272         RetainPtr<NSAttributedString> selection = adoptNS([[NSAttributedString alloc] initWithRTFD:selectionData.get() documentAttributes:nil]);
273
274         items = @[ selection.get() ];
275     } else {
276         LOG_ERROR("No service controlled item represented in the context");
277         return;
278     }
279
280     RetainPtr<NSSharingServicePicker> picker = adoptNS([[NSSharingServicePicker alloc] initWithItems:items]);
281     [picker setStyle:hasControlledImage ? NSSharingServicePickerStyleRollover : NSSharingServicePickerStyleTextSelection];
282     [picker setDelegate:[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate]];
283     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setPicker:picker.get()];
284     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setFiltersEditingServices:!includeEditorServices];
285     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setHandlesEditingReplacement:includeEditorServices];
286
287     m_servicesMenu = adoptNS([[picker menu] copy]);
288
289     if (!hasControlledImage)
290         [m_servicesMenu setShowsStateColumn:YES];
291
292     // Explicitly add a menu item for each telephone number that is in the selection.
293     const Vector<String>& selectedTelephoneNumbers = context.selectedTelephoneNumbers();
294     Vector<RetainPtr<NSMenuItem>> telephoneNumberMenuItems;
295     for (auto& telephoneNumber : selectedTelephoneNumbers) {
296         if (NSMenuItem *item = menuItemForTelephoneNumber(telephoneNumber)) {
297             [item setIndentationLevel:1];
298             telephoneNumberMenuItems.append(item);
299         }
300     }
301
302     if (!telephoneNumberMenuItems.isEmpty()) {
303         if (m_servicesMenu)
304             [m_servicesMenu insertItem:[NSMenuItem separatorItem] atIndex:0];
305         else
306             m_servicesMenu = adoptNS([[NSMenu alloc] init]);
307         int itemPosition = 0;
308         NSMenuItem *groupEntry = [[NSMenuItem alloc] initWithTitle:menuItemTitleForTelephoneNumberGroup() action:nil keyEquivalent:@""];
309         [groupEntry setEnabled:NO];
310         [m_servicesMenu insertItem:groupEntry atIndex:itemPosition++];
311         for (auto& menuItem : telephoneNumberMenuItems)
312             [m_servicesMenu insertItem:menuItem.get() atIndex:itemPosition++];
313     }
314
315     // If there is no services menu, then the existing services on the system have changed, so refresh that list of services.
316     // If <rdar://problem/17954709> is resolved then we can more accurately keep the list up to date without this call.
317     if (!m_servicesMenu)
318         ServicesController::singleton().refreshExistingServices();
319 }
320
321 void WebContextMenuProxyMac::clearServicesMenu()
322 {
323     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setPicker:nullptr];
324     m_servicesMenu = nullptr;
325 }
326
327 ContextMenuItem WebContextMenuProxyMac::shareMenuItem()
328 {
329     const WebHitTestResultData& hitTestData = m_context.webHitTestResultData();
330
331     URL absoluteLinkURL;
332     if (!hitTestData.absoluteLinkURL.isEmpty())
333         absoluteLinkURL = URL(ParsedURLString, hitTestData.absoluteLinkURL);
334
335     URL downloadableMediaURL;
336     if (!hitTestData.absoluteMediaURL.isEmpty() && hitTestData.isDownloadableMedia)
337         downloadableMediaURL = URL(ParsedURLString, hitTestData.absoluteMediaURL);
338
339     RetainPtr<NSImage> image;
340     if (hitTestData.imageSharedMemory && hitTestData.imageSize)
341         image = adoptNS([[NSImage alloc] initWithData:[NSData dataWithBytes:(unsigned char*)hitTestData.imageSharedMemory->data() length:hitTestData.imageSize]]);
342
343     ContextMenuItem item = ContextMenuItem::shareMenuItem(absoluteLinkURL, downloadableMediaURL, image.get(), m_context.selectedText());
344     if (item.isNull())
345         return item;
346
347     NSMenuItem *nsItem = item.platformDescription();
348
349     NSSharingServicePicker *sharingServicePicker = [nsItem representedObject];
350     sharingServicePicker.delegate = [WKSharingServicePickerDelegate sharedSharingServicePickerDelegate];
351
352     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setFiltersEditingServices:NO];
353     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setHandlesEditingReplacement:NO];
354     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setMenuProxy:this];
355
356     // Setting the picker lets the delegate retain it to keep it alive, but this picker is kept alive by the menu item.
357     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setPicker:nil];
358
359     return item;
360 }
361 #endif
362
363 void WebContextMenuProxyMac::populate(const Vector<RefPtr<WebContextMenuItem>>& items)
364 {
365 #if ENABLE(SERVICE_CONTROLS)
366     if (m_context.isServicesMenu()) {
367         setupServicesMenu(m_context);
368         return;
369     }
370 #endif
371
372     if (m_popup)
373         [m_popup removeAllItems];
374     else {
375         m_popup = adoptNS([[NSPopUpButtonCell alloc] initTextCell:@"" pullsDown:NO]);
376         [m_popup setUsesItemFromMenu:NO];
377         [m_popup setAutoenablesItems:NO];
378         [m_popup setAltersStateOfSelectedItem:NO];
379     }
380
381     NSMenu* menu = [m_popup menu];
382     populateNSMenu(menu, nsMenuItemVector(items));
383 }
384
385 void WebContextMenuProxyMac::showContextMenu()
386 {
387     // Unless this is an image control, give the PageContextMenuClient one last swipe at changing the menu.
388     bool askClientToChangeMenu = true;
389 #if ENABLE(SERVICE_CONTROLS)
390     if (m_context.isServicesMenu() || m_context.controlledImage())
391         askClientToChangeMenu = false;
392 #endif
393
394     Vector<RefPtr<WebContextMenuItem>> proposedAPIItems;
395     for (auto& item : m_context.menuItems()) {
396         if (item.action() != ContextMenuItemTagShareMenu) {
397             proposedAPIItems.append(WebContextMenuItem::create(item));
398             continue;
399         }
400
401 #if ENABLE(SERVICE_CONTROLS)
402         ContextMenuItem shareItem = shareMenuItem();
403         if (!shareItem.isNull())
404             proposedAPIItems.append(WebContextMenuItem::create(shareItem));
405 #endif
406     }
407
408     Vector<RefPtr<WebContextMenuItem>> clientItems;
409     bool useProposedItems = true;
410
411     if (askClientToChangeMenu && m_page.contextMenuClient().getContextMenuFromProposedMenu(m_page, proposedAPIItems, clientItems, m_context.webHitTestResultData(), m_page.process().transformHandlesToObjects(m_userData.object()).get()))
412         useProposedItems = false;
413
414     const Vector<RefPtr<WebContextMenuItem>>& items = useProposedItems ? proposedAPIItems : clientItems;
415
416 #if ENABLE(SERVICE_CONTROLS)
417     if (items.isEmpty() && !m_context.isServicesMenu())
418         return;
419 #else
420     if (items.isEmpty())
421         return;
422 #endif
423
424     populate(items);
425
426     [[WKMenuTarget sharedMenuTarget] setMenuProxy:this];
427
428     NSPoint menuLocation = m_context.menuLocation();
429     NSRect menuRect = NSMakeRect(menuLocation.x, menuLocation.y, 0, 0);
430
431 #if ENABLE(SERVICE_CONTROLS)
432     if (m_context.isServicesMenu())
433         [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setMenuProxy:this];
434
435     if (!m_servicesMenu)
436         [m_popup attachPopUpWithFrame:menuRect inView:m_webView];
437
438     NSMenu *menu = m_servicesMenu ? m_servicesMenu.get() : [m_popup menu];
439
440     // Telephone number and service menus must use the [NSMenu popUpMenuPositioningItem:atLocation:inView:] API.
441     // FIXME: That API is better than WKPopupContextMenu. In the future all menus should use either it
442     // or the [NSMenu popUpContextMenu:withEvent:forView:] API, depending on the menu type.
443     // Then we could get rid of NSPopUpButtonCell, custom metrics, and WKPopupContextMenu.
444     if (m_context.isServicesMenu()) {
445         [menu popUpMenuPositioningItem:nil atLocation:menuLocation inView:m_webView];
446         hideContextMenu();
447         return;
448     }
449
450 #else
451     [m_popup attachPopUpWithFrame:menuRect inView:m_webView];
452
453     NSMenu *menu = [m_popup menu];
454 #endif
455
456     // These values were borrowed from AppKit to match their placement of the menu.
457     NSRect titleFrame = [m_popup titleRectForBounds:menuRect];
458     if (titleFrame.size.width <= 0 || titleFrame.size.height <= 0)
459         titleFrame = menuRect;
460     float vertOffset = roundf((NSMaxY(menuRect) - NSMaxY(titleFrame)) + NSHeight(titleFrame));
461     NSPoint location = NSMakePoint(NSMinX(menuRect), NSMaxY(menuRect) - vertOffset);
462
463     location = [m_webView convertPoint:location toView:nil];
464 #pragma clang diagnostic push
465 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
466     location = [m_webView.window convertBaseToScreen:location];
467 #pragma clang diagnostic pop
468
469     Ref<WebContextMenuProxyMac> protect(*this);
470
471     WKPopupContextMenu(menu, location);
472
473     hideContextMenu();
474 }
475
476 void WebContextMenuProxyMac::hideContextMenu()
477 {
478     [m_popup dismissPopUp];
479 }
480
481 void WebContextMenuProxyMac::cancelTracking()
482 {
483     [[m_popup menu] cancelTracking];
484 }
485
486 NSWindow *WebContextMenuProxyMac::window() const
487 {
488     return [m_webView window];
489 }
490
491 } // namespace WebKit
492
493 #endif // PLATFORM(MAC)