448c0bdd3cb5399ce4092e0e1060eea151d3cf6b
[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_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 ContextMenuItem WebContextMenuProxyMac::shareMenuItem()
261 {
262     const WebHitTestResultData& hitTestData = m_context.webHitTestResultData();
263
264     URL absoluteLinkURL;
265     if (!hitTestData.absoluteLinkURL.isEmpty())
266         absoluteLinkURL = URL(ParsedURLString, hitTestData.absoluteLinkURL);
267
268     URL downloadableMediaURL;
269     if (!hitTestData.absoluteMediaURL.isEmpty() && hitTestData.isDownloadableMedia)
270         downloadableMediaURL = URL(ParsedURLString, hitTestData.absoluteMediaURL);
271
272     RetainPtr<NSImage> image;
273     if (hitTestData.imageSharedMemory && hitTestData.imageSize)
274         image = adoptNS([[NSImage alloc] initWithData:[NSData dataWithBytes:(unsigned char*)hitTestData.imageSharedMemory->data() length:hitTestData.imageSize]]);
275
276     ContextMenuItem item = ContextMenuItem::shareMenuItem(absoluteLinkURL, downloadableMediaURL, image.get(), m_context.selectedText());
277     if (item.isNull())
278         return item;
279
280     NSMenuItem *nsItem = item.platformDescription();
281
282     NSSharingServicePicker *sharingServicePicker = [nsItem representedObject];
283     sharingServicePicker.delegate = [WKSharingServicePickerDelegate sharedSharingServicePickerDelegate];
284
285     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setFiltersEditingServices:NO];
286     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setHandlesEditingReplacement:NO];
287     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setMenuProxy:this];
288
289     // Setting the picker lets the delegate retain it to keep it alive, but this picker is kept alive by the menu item.
290     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setPicker:nil];
291
292     return item;
293 }
294 #endif
295
296 void WebContextMenuProxyMac::show()
297 {
298     Ref<WebPageProxy> protect(m_page);
299
300 #if ENABLE(SERVICE_CONTROLS)
301     if (m_context.isServicesMenu()) {
302         showServicesMenu();
303         return;
304     }
305 #endif
306
307     showContextMenu();
308 }
309
310 RetainPtr<NSMenu> WebContextMenuProxyMac::createContextMenuFromItems(const Vector<WebContextMenuItemData>& items)
311 {
312     auto menu = adoptNS([[NSMenu alloc] initWithTitle:@""]);
313     [menu setAutoenablesItems:NO];
314
315     for (auto& item : items) {
316         if (auto menuItem = createContextMenuItem(item))
317             [menu addItem:menuItem.get()];
318     }
319
320     return menu;
321 }
322
323 RetainPtr<NSMenuItem> WebContextMenuProxyMac::createContextMenuItem(const WebContextMenuItemData& item)
324 {
325 #if ENABLE(SERVICE_CONTROLS)
326     if (item.action() == ContextMenuItemTagShareMenu)
327         return shareMenuItem().platformDescription();
328 #endif
329
330     switch (item.type()) {
331     case ActionType:
332     case CheckableActionType: {
333         auto menuItem = adoptNS([[NSMenuItem alloc] initWithTitle:item.title() action:@selector(forwardContextMenuAction:) keyEquivalent:@""]);
334
335         [menuItem setTag:item.action()];
336         [menuItem setEnabled:item.enabled()];
337         [menuItem setState:item.checked() ? NSOnState : NSOffState];
338         [menuItem setTarget:[WKMenuTarget sharedMenuTarget]];
339
340         if (item.userData()) {
341             auto wrapper = adoptNS([[WKUserDataWrapper alloc] initWithUserData:item.userData()]);
342             [menuItem setRepresentedObject:wrapper.get()];
343         }
344
345         return menuItem;
346     }
347
348     case SeparatorType:
349         return [NSMenuItem separatorItem];
350
351     case SubmenuType: {
352         auto menuItem = adoptNS([[NSMenuItem alloc] initWithTitle:item.title() action:nullptr keyEquivalent:@""]);
353         [menuItem setEnabled:item.enabled()];
354         [menuItem setSubmenu:createContextMenuFromItems(item.submenu()).get()];
355
356         return menuItem;
357     }
358     }
359 }
360
361 void WebContextMenuProxyMac::showContextMenu()
362 {
363     Vector<RefPtr<WebContextMenuItem>> proposedAPIItems;
364     for (auto& item : m_context.menuItems())
365         proposedAPIItems.append(WebContextMenuItem::create(item));
366
367     Vector<RefPtr<WebContextMenuItem>> clientItems;
368     bool useProposedItems = true;
369
370     // FIXME: Get rid of this once we don't need the C SPI.
371     if (m_page.contextMenuClient().getContextMenuFromProposedMenu(m_page, proposedAPIItems, clientItems, m_context.webHitTestResultData(), m_page.process().transformHandlesToObjects(m_userData.object()).get()))
372         useProposedItems = false;
373
374     Vector<WebContextMenuItemData> items;
375     for (auto& item : (useProposedItems ? proposedAPIItems : clientItems))
376         items.append(item->data());
377
378     if (items.isEmpty())
379         return;
380
381     auto menu = createContextMenuFromItems(items);
382     m_menu = m_page.contextMenuClient().menuFromProposedMenu(m_page, menu.get(), m_context.webHitTestResultData());
383
384     [[WKMenuTarget sharedMenuTarget] setMenuProxy:this];
385     [m_menu popUpMenuPositioningItem:nil atLocation:m_context.menuLocation() inView:m_webView];
386 }
387
388 NSWindow *WebContextMenuProxyMac::window() const
389 {
390     return [m_webView window];
391 }
392
393 } // namespace WebKit
394
395 #endif // PLATFORM(MAC)