If there are no services available, do not show the service controls UI
[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 "DataReference.h"
32 #import "PageClientImpl.h"
33 #import "ShareableBitmap.h"
34 #import "StringUtilities.h"
35 #import "WebContext.h"
36 #import "WebContextMenuItemData.h"
37 #import "WebProcessProxy.h"
38 #import "WKView.h"
39 #import <WebCore/GraphicsContext.h>
40 #import <WebCore/IntRect.h>
41 #import <WebCore/NotImplemented.h>
42 #import <WebKitSystemInterface.h>
43 #import <wtf/RetainPtr.h>
44
45 #if ENABLE(SERVICE_CONTROLS)
46 #import <AppKit/NSSharingService.h>
47
48 #if __has_include(<AppKit/NSSharingService_Private.h>)
49 #import <AppKit/NSSharingService_Private.h>
50 #else
51 typedef enum {
52     NSSharingServicePickerStyleMenu = 0,
53     NSSharingServicePickerStyleRollover = 1,
54     NSSharingServicePickerStyleTextSelection = 2
55 } NSSharingServicePickerStyle;
56 #endif
57
58 @interface NSSharingServicePicker (Details)
59 @property NSSharingServicePickerStyle style;
60 - (NSMenu *)menu;
61 @end
62
63 #endif // ENABLE(SERVICE_CONTROLS)
64
65 using namespace WebCore;
66
67 @interface WKUserDataWrapper : NSObject {
68     RefPtr<API::Object> _webUserData;
69 }
70 - (id)initWithUserData:(API::Object*)userData;
71 - (API::Object*)userData;
72 @end
73
74 @implementation WKUserDataWrapper
75
76 - (id)initWithUserData:(API::Object*)userData
77 {
78     self = [super init];
79     if (!self)
80         return nil;
81     
82     _webUserData = userData;
83     return self;
84 }
85
86 - (API::Object*)userData
87 {
88     return _webUserData.get();
89 }
90
91 @end
92
93 @interface WKSelectionHandlerWrapper : NSObject {
94     std::function<void()> _selectionHandler;
95 }
96 - (id)initWithSelectionHandler:(std::function<void()>)selectionHandler;
97 - (void)executeSelectionHandler;
98 @end
99
100 @implementation WKSelectionHandlerWrapper
101 - (id)initWithSelectionHandler:(std::function<void()>)selectionHandler
102 {
103     self = [super init];
104     if (!self)
105         return nil;
106     
107     _selectionHandler = selectionHandler;
108     return self;
109 }
110
111 - (void)executeSelectionHandler
112 {
113     if (_selectionHandler)
114         _selectionHandler();
115 }
116 @end
117
118 @interface WKMenuTarget : NSObject {
119     WebKit::WebContextMenuProxyMac* _menuProxy;
120 }
121 + (WKMenuTarget *)sharedMenuTarget;
122 - (WebKit::WebContextMenuProxyMac*)menuProxy;
123 - (void)setMenuProxy:(WebKit::WebContextMenuProxyMac*)menuProxy;
124 - (void)forwardContextMenuAction:(id)sender;
125 @end
126
127 @implementation WKMenuTarget
128
129 + (WKMenuTarget*)sharedMenuTarget
130 {
131     static WKMenuTarget* target = [[WKMenuTarget alloc] init];
132     return target;
133 }
134
135 - (WebKit::WebContextMenuProxyMac*)menuProxy
136 {
137     return _menuProxy;
138 }
139
140 - (void)setMenuProxy:(WebKit::WebContextMenuProxyMac*)menuProxy
141 {
142     _menuProxy = menuProxy;
143 }
144
145 - (void)forwardContextMenuAction:(id)sender
146 {
147     id representedObject = [sender representedObject];
148
149     // NSMenuItems with a represented selection handler belong solely to the UI process
150     // and don't need any further processing after the selection handler is called.
151     if ([representedObject isKindOfClass:[WKSelectionHandlerWrapper class]]) {
152         [representedObject executeSelectionHandler];
153         return;
154     }
155
156     WebKit::WebContextMenuItemData item(ActionType, static_cast<ContextMenuAction>([sender tag]), [sender title], [sender isEnabled], [sender state] == NSOnState);
157     if (representedObject) {
158         ASSERT([representedObject isKindOfClass:[WKUserDataWrapper class]]);
159         item.setUserData([static_cast<WKUserDataWrapper *>(representedObject) userData]);
160     }
161             
162     _menuProxy->contextMenuItemSelected(item);
163 }
164
165 @end
166
167 #if ENABLE(SERVICE_CONTROLS)
168 @interface WKSharingServicePickerDelegate : NSObject <NSSharingServiceDelegate, NSSharingServicePickerDelegate> {
169     WebKit::WebContextMenuProxyMac* _menuProxy;
170     RetainPtr<NSSharingServicePicker> _picker;
171     BOOL _includeEditorServices;
172 }
173
174 + (WKSharingServicePickerDelegate *)sharedSharingServicePickerDelegate;
175 - (WebKit::WebContextMenuProxyMac*)menuProxy;
176 - (void)setMenuProxy:(WebKit::WebContextMenuProxyMac*)menuProxy;
177 - (void)setPicker:(NSSharingServicePicker *)picker;
178 - (void)setIncludeEditorServices:(BOOL)includeEditorServices;
179 @end
180
181 // FIXME: We probably need to hang on the picker itself until the context menu operation is done, and this object will probably do that.
182 @implementation WKSharingServicePickerDelegate
183 + (WKSharingServicePickerDelegate*)sharedSharingServicePickerDelegate
184 {
185     static WKSharingServicePickerDelegate* delegate = [[WKSharingServicePickerDelegate alloc] init];
186     return delegate;
187 }
188
189 - (WebKit::WebContextMenuProxyMac*)menuProxy
190 {
191     return _menuProxy;
192 }
193
194 - (void)setMenuProxy:(WebKit::WebContextMenuProxyMac*)menuProxy
195 {
196     _menuProxy = menuProxy;
197 }
198
199 - (void)setPicker:(NSSharingServicePicker *)picker
200 {
201     _picker = picker;
202 }
203
204 - (void)setIncludeEditorServices:(BOOL)includeEditorServices
205 {
206     _includeEditorServices = includeEditorServices;
207 }
208
209 - (NSArray *)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker sharingServicesForItems:(NSArray *)items mask:(NSSharingServiceMask)mask proposedSharingServices:(NSArray *)proposedServices
210 {
211     if (_includeEditorServices)
212         return proposedServices;
213
214     NSMutableArray *services = [[NSMutableArray alloc] initWithCapacity:[proposedServices count]];
215     
216     for (NSSharingService *service in proposedServices) {
217         if (service.type != NSSharingServiceTypeEditor)
218             [services addObject:service];
219     }
220     
221     return services;
222 }
223
224 - (id <NSSharingServiceDelegate>)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker delegateForSharingService:(NSSharingService *)sharingService
225 {
226     return self;
227 }
228
229 - (void)sharingService:(NSSharingService *)sharingService willShareItems:(NSArray *)items
230 {
231     _menuProxy->clearServicesMenu();
232 }
233
234 - (void)sharingService:(NSSharingService *)sharingService didShareItems:(NSArray *)items
235 {
236     // We only care about what item was shared if we were interested in editor services
237     // (i.e., if we plan on replacing the selection with the returned item)
238     if (!_includeEditorServices)
239         return;
240
241     Vector<String> types;
242     IPC::DataReference dataReference;
243
244     id item = [items objectAtIndex:0];
245
246     if ([item isKindOfClass:[NSAttributedString class]]) {
247         NSData *data = [item RTFDFromRange:NSMakeRange(0, [item length]) documentAttributes:nil];
248         dataReference = IPC::DataReference(static_cast<const uint8_t*>([data bytes]), [data length]);
249
250         types.append(NSPasteboardTypeRTFD);
251         types.append(NSRTFDPboardType);
252     } else if ([item isKindOfClass:[NSData class]]) {
253         NSData *data = (NSData *)item;
254         RetainPtr<CGImageSourceRef> source = adoptCF(CGImageSourceCreateWithData((CFDataRef)data, NULL));
255         RetainPtr<CGImageRef> image = adoptCF(CGImageSourceCreateImageAtIndex(source.get(), 0, NULL));
256
257         if (!image)
258             return;
259
260         dataReference = IPC::DataReference(static_cast<const uint8_t*>([data bytes]), [data length]);
261         types.append(NSPasteboardTypeTIFF);
262     } else {
263         LOG_ERROR("sharingService:didShareItems: - Unknown item type returned\n");
264         return;
265     }
266
267     _menuProxy->page().replaceSelectionWithPasteboardData(types, dataReference);
268 }
269
270 - (NSWindow *)sharingService:(NSSharingService *)sharingService sourceWindowForShareItems:(NSArray *)items sharingContentScope:(NSSharingContentScope *)sharingContentScope
271 {
272     return _menuProxy->window();
273 }
274
275 @end
276
277 #endif
278
279 namespace WebKit {
280
281 WebContextMenuProxyMac::WebContextMenuProxyMac(WKView* webView, WebPageProxy* page)
282     : m_webView(webView)
283     , m_page(page)
284 {
285     ASSERT(m_page);
286 }
287
288 WebContextMenuProxyMac::~WebContextMenuProxyMac()
289 {
290     if (m_popup)
291         [m_popup setControlView:nil];
292 }
293
294 void WebContextMenuProxyMac::contextMenuItemSelected(const WebContextMenuItemData& item)
295 {
296 #if ENABLE(SERVICE_CONTROLS)
297     clearServicesMenu();
298 #endif
299
300     m_page->contextMenuItemSelected(item);
301 }
302
303 static void populateNSMenu(NSMenu* menu, const Vector<RetainPtr<NSMenuItem>>& menuItemVector)
304 {
305     for (unsigned i = 0; i < menuItemVector.size(); ++i) {
306         NSInteger oldState = [menuItemVector[i].get() state];
307         [menu addItem:menuItemVector[i].get()];
308         [menuItemVector[i].get() setState:oldState];
309     }
310 }
311
312 static Vector<RetainPtr<NSMenuItem>> nsMenuItemVector(const Vector<WebContextMenuItemData>& items)
313 {
314     Vector<RetainPtr<NSMenuItem>> result;
315
316     unsigned size = items.size();
317     result.reserveCapacity(size);
318     for (unsigned i = 0; i < size; i++) {
319         switch (items[i].type()) {
320         case ActionType:
321         case CheckableActionType: {
322             NSMenuItem* menuItem = [[NSMenuItem alloc] initWithTitle:nsStringFromWebCoreString(items[i].title()) action:@selector(forwardContextMenuAction:) keyEquivalent:@""];
323             [menuItem setTag:items[i].action()];
324             [menuItem setEnabled:items[i].enabled()];
325             [menuItem setState:items[i].checked() ? NSOnState : NSOffState];
326
327             if (std::function<void()> selectionHandler = items[i].selectionHandler()) {
328                 WKSelectionHandlerWrapper *wrapper = [[WKSelectionHandlerWrapper alloc] initWithSelectionHandler:selectionHandler];
329                 [menuItem setRepresentedObject:wrapper];
330                 [wrapper release];
331             } else if (items[i].userData()) {
332                 WKUserDataWrapper *wrapper = [[WKUserDataWrapper alloc] initWithUserData:items[i].userData()];
333                 [menuItem setRepresentedObject:wrapper];
334                 [wrapper release];
335             }
336
337             result.append(adoptNS(menuItem));
338             break;
339         }
340         case SeparatorType:
341             result.append([NSMenuItem separatorItem]);
342             break;
343         case SubmenuType: {
344             NSMenu* menu = [[NSMenu alloc] initWithTitle:nsStringFromWebCoreString(items[i].title())];
345             [menu setAutoenablesItems:NO];
346             populateNSMenu(menu, nsMenuItemVector(items[i].submenu()));
347                 
348             NSMenuItem* menuItem = [[NSMenuItem alloc] initWithTitle:nsStringFromWebCoreString(items[i].title()) action:@selector(forwardContextMenuAction:) keyEquivalent:@""];
349             [menuItem setEnabled:items[i].enabled()];
350             [menuItem setSubmenu:menu];
351             [menu release];
352
353             result.append(adoptNS(menuItem));
354             
355             break;
356         }
357         default:
358             ASSERT_NOT_REACHED();
359         }
360     }
361
362     WKMenuTarget* target = [WKMenuTarget sharedMenuTarget];
363     for (unsigned i = 0; i < size; ++i)
364         [result[i].get() setTarget:target];
365     
366     return result;
367 }
368
369 #if ENABLE(SERVICE_CONTROLS)
370
371 void WebContextMenuProxyMac::setupServicesMenu(const ContextMenuContextData& context)
372 {
373     RetainPtr<NSSharingServicePicker> picker;
374     bool includeEditorServices = context.controlledDataIsEditable();
375     NSArray *items = nil;
376     if (!context.controlledImageHandle().isNull()) {
377         RefPtr<ShareableBitmap> image = ShareableBitmap::create(context.controlledImageHandle());
378         if (!image)
379             return;
380
381         RetainPtr<CGImageRef> cgImage = image->makeCGImage();
382         RetainPtr<NSImage> nsImage = adoptNS([[NSImage alloc] initWithCGImage:cgImage.get() size:image->size()]);
383         items = @[ nsImage.get() ];
384     } else if (!context.controlledSelectionData().isEmpty()) {
385         RetainPtr<NSData> selectionData = adoptNS([[NSData alloc] initWithBytes:(void*)context.controlledSelectionData().data() length:context.controlledSelectionData().size()]);
386         RetainPtr<NSAttributedString> selection = adoptNS([[NSAttributedString alloc] initWithRTFD:selectionData.get() documentAttributes:nil]);
387
388         items = @[ selection.get() ];
389     } else {
390         LOG_ERROR("No service controlled item represented in the context");
391         return;
392     }
393
394     picker = adoptNS([[NSSharingServicePicker alloc] initWithItems:items]);
395     [picker setStyle:NSSharingServicePickerStyleRollover];
396     [picker setDelegate:[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate]];
397     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setPicker:picker.get()];
398     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setIncludeEditorServices:includeEditorServices];
399
400     m_servicesMenu = [picker menu];
401
402     // If there is no services menu, then the existing services on the system have changed.
403     // Ask the UIProcess to refresh that list of services.
404     // If <rdar://problem/16776831> is resolved then we can more accurately keep the list up to date without this call.
405     if (!m_servicesMenu)
406         m_page->process().context().refreshExistingServices();
407 }
408
409 void WebContextMenuProxyMac::clearServicesMenu()
410 {
411     [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setPicker:nullptr];
412     m_servicesMenu = nullptr;
413 }
414 #endif
415
416 void WebContextMenuProxyMac::populate(const Vector<WebContextMenuItemData>& items, const ContextMenuContextData& context)
417 {
418 #if ENABLE(SERVICE_CONTROLS)
419     if (context.needsServicesMenu()) {
420         setupServicesMenu(context);
421         return;
422     }
423 #endif
424
425     if (m_popup)
426         [m_popup removeAllItems];
427     else {
428         m_popup = adoptNS([[NSPopUpButtonCell alloc] initTextCell:@"" pullsDown:NO]);
429         [m_popup setUsesItemFromMenu:NO];
430         [m_popup setAutoenablesItems:NO];
431     }
432
433     NSMenu* menu = [m_popup menu];
434     populateNSMenu(menu, nsMenuItemVector(items));
435 }
436
437 void WebContextMenuProxyMac::showContextMenu(const IntPoint& menuLocation, const Vector<WebContextMenuItemData>& items, const ContextMenuContextData& context)
438 {
439 #if ENABLE(SERVICE_CONTROLS)
440     if (items.isEmpty() && !context.needsServicesMenu())
441         return;
442 #else
443     if (items.isEmpty())
444         return;
445 #endif
446
447     populate(items, context);
448
449     [[WKMenuTarget sharedMenuTarget] setMenuProxy:this];
450
451     NSRect menuRect = NSMakeRect(menuLocation.x(), menuLocation.y(), 0, 0);
452
453 #if ENABLE(SERVICE_CONTROLS)
454     if (context.needsServicesMenu())
455         [[WKSharingServicePickerDelegate sharedSharingServicePickerDelegate] setMenuProxy:this];
456
457     if (!m_servicesMenu)
458         [m_popup attachPopUpWithFrame:menuRect inView:m_webView];
459
460     NSMenu *menu = m_servicesMenu ? m_servicesMenu.get() : [m_popup menu];
461 #else
462     [m_popup attachPopUpWithFrame:menuRect inView:m_webView];
463
464     NSMenu *menu = [m_popup menu];
465 #endif
466
467     // These values were borrowed from AppKit to match their placement of the menu.
468     NSRect titleFrame = [m_popup titleRectForBounds:menuRect];
469     if (titleFrame.size.width <= 0 || titleFrame.size.height <= 0)
470         titleFrame = menuRect;
471     float vertOffset = roundf((NSMaxY(menuRect) - NSMaxY(titleFrame)) + NSHeight(titleFrame));
472     NSPoint location = NSMakePoint(NSMinX(menuRect), NSMaxY(menuRect) - vertOffset);
473
474     location = [m_webView convertPoint:location toView:nil];
475 #pragma clang diagnostic push
476 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
477     location = [m_webView.window convertBaseToScreen:location];
478 #pragma clang diagnostic pop
479
480     WKPopupContextMenu(menu, location);
481
482     hideContextMenu();
483 }
484
485 void WebContextMenuProxyMac::hideContextMenu()
486 {
487     [m_popup dismissPopUp];
488 }
489
490 NSWindow *WebContextMenuProxyMac::window() const
491 {
492     return [m_webView window];
493 }
494
495 } // namespace WebKit
496
497 #endif // PLATFORM(MAC)