Remove "Add to iPhoto" from the action menu's sharing menu
[WebKit-https.git] / Source / WebKit2 / UIProcess / mac / WKActionMenuController.mm
1 /*
2  * Copyright (C) 2014 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 "WKActionMenuController.h"
28
29 #if PLATFORM(MAC)
30
31 #import "WKNSURLExtras.h"
32 #import "WKViewInternal.h"
33 #import "WebContext.h"
34 #import "WebKitSystemInterface.h"
35 #import "WebPageMessages.h"
36 #import "WebPageProxy.h"
37 #import "WebPageProxyMessages.h"
38 #import "WebProcessProxy.h"
39 #import <Foundation/Foundation.h>
40 #import <ImageIO/ImageIO.h>
41 #import <ImageKit/ImageKit.h>
42 #import <WebCore/DataDetectorsSPI.h>
43 #import <WebCore/NSSharingServicePickerSPI.h>
44 #import <WebCore/NSViewSPI.h>
45 #import <WebCore/SoftLinking.h>
46 #import <WebCore/URL.h>
47
48 // FIXME: This should move into an SPI header if it stays.
49 @class QLPreviewBubble;
50 @interface NSObject (WKQLPreviewBubbleDetails)
51 @property (copy) NSArray * controls;
52 @property NSSize maximumSize;
53 @property NSRectEdge preferredEdge;
54 @property (retain) IBOutlet NSWindow* parentWindow;
55 - (void)showPreviewItem:(id)previewItem itemFrame:(NSRect)frame;
56 - (void)setAutomaticallyCloseWithMask:(NSEventMask)autocloseMask filterMask:(NSEventMask)filterMask block:(void (^)(void))block;
57 @end
58
59 SOFT_LINK_FRAMEWORK_IN_UMBRELLA(Quartz, ImageKit)
60 SOFT_LINK_CLASS(ImageKit, IKSlideshow)
61
62 using namespace WebCore;
63 using namespace WebKit;
64
65 @interface WKActionMenuController () <NSSharingServiceDelegate, NSSharingServicePickerDelegate>
66 - (void)_updateActionMenuItems;
67 - (BOOL)_canAddImageToPhotos;
68 @end
69
70 @interface WKView (WKDeprecatedSPI)
71 - (NSArray *)_actionMenuItemsForHitTestResult:(WKHitTestResultRef)hitTestResult defaultActionMenuItems:(NSArray *)defaultMenuItems;
72 @end
73
74 @implementation WKActionMenuController
75
76 - (instancetype)initWithPage:(WebPageProxy&)page view:(WKView *)wkView
77 {
78     self = [super init];
79
80     if (!self)
81         return nil;
82
83     _page = &page;
84     _wkView = wkView;
85     _type = kWKActionMenuNone;
86
87     return self;
88 }
89
90 - (void)willDestroyView:(WKView *)view
91 {
92     _page = nullptr;
93     _wkView = nullptr;
94 }
95
96 - (void)prepareForMenu:(NSMenu *)menu withEvent:(NSEvent *)event
97 {
98     if (menu != _wkView.actionMenu)
99         return;
100
101     _page->performActionMenuHitTestAtLocation([_wkView convertPoint:event.locationInWindow fromView:nil]);
102
103     _state = ActionMenuState::Pending;
104     [self _updateActionMenuItems];
105 }
106
107 - (void)willOpenMenu:(NSMenu *)menu withEvent:(NSEvent *)event
108 {
109     if (menu != _wkView.actionMenu)
110         return;
111
112     if (_type != kWKActionMenuReadOnlyText)
113         return;
114
115     // Action menus for text should highlight the text so that it is clear what the action menu actions
116     // will apply to. If the text is already selected, the menu will use the existing selection.
117     WebHitTestResult* hitTestResult = _page->lastMouseMoveHitTestResult();
118     if (!hitTestResult->isSelected())
119         _page->selectLookupTextAtLocation([_wkView convertPoint:event.locationInWindow fromView:nil]);
120 }
121
122 - (void)didCloseMenu:(NSMenu *)menu withEvent:(NSEvent *)event
123 {
124     if (menu != _wkView.actionMenu)
125         return;
126     
127     _state = ActionMenuState::None;
128     _hitTestResult = ActionMenuHitTestResult();
129     _type = kWKActionMenuNone;
130     _sharingServicePicker = nil;
131 }
132
133 - (void)didPerformActionMenuHitTest:(const ActionMenuHitTestResult&)hitTestResult
134 {
135     // FIXME: This needs to use the WebKit2 callback mechanism to avoid out-of-order replies.
136     _state = ActionMenuState::Ready;
137     _hitTestResult = hitTestResult;
138 }
139
140 #pragma mark Link actions
141
142 - (NSArray *)_defaultMenuItemsForLink
143 {
144     WebHitTestResult* hitTestResult = _page->lastMouseMoveHitTestResult();
145     if (!WebCore::protocolIsInHTTPFamily(hitTestResult->absoluteLinkURL()))
146         return @[ ];
147
148     RetainPtr<NSMenuItem> openLinkItem = [self _createActionMenuItemForTag:kWKContextActionItemTagOpenLinkInDefaultBrowser];
149     RetainPtr<NSMenuItem> previewLinkItem = [self _createActionMenuItemForTag:kWKContextActionItemTagPreviewLink];
150     RetainPtr<NSMenuItem> readingListItem = [self _createActionMenuItemForTag:kWKContextActionItemTagAddLinkToSafariReadingList];
151
152     return @[ openLinkItem.get(), previewLinkItem.get(), [NSMenuItem separatorItem], readingListItem.get() ];
153 }
154
155 - (void)_openURLFromActionMenu:(id)sender
156 {
157     WebHitTestResult* hitTestResult = _page->lastMouseMoveHitTestResult();
158     [[NSWorkspace sharedWorkspace] openURL:[NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()]];
159 }
160
161 - (void)_addToReadingListFromActionMenu:(id)sender
162 {
163     WebHitTestResult* hitTestResult = _page->lastMouseMoveHitTestResult();
164     NSSharingService *service = [NSSharingService sharingServiceNamed:NSSharingServiceNameAddToSafariReadingList];
165     [service performWithItems:@[ [NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()] ]];
166 }
167
168 - (void)_quickLookURLFromActionMenu:(id)sender
169 {
170     WebHitTestResult* hitTestResult = _page->lastMouseMoveHitTestResult();
171     NSRect itemFrame = [_wkView convertRect:hitTestResult->elementBoundingBox() toView:nil];
172     NSSize maximumPreviewSize = NSMakeSize(_wkView.bounds.size.width * 0.75, _wkView.bounds.size.height * 0.75);
173
174     RetainPtr<QLPreviewBubble> bubble = adoptNS([[NSClassFromString(@"QLPreviewBubble") alloc] init]);
175     [bubble setParentWindow:_wkView.window];
176     [bubble setMaximumSize:maximumPreviewSize];
177     [bubble setPreferredEdge:NSMaxYEdge];
178     [bubble setControls:@[ ]];
179     NSEventMask filterMask = NSAnyEventMask & ~(NSAppKitDefinedMask | NSSystemDefinedMask | NSApplicationDefinedMask | NSMouseEnteredMask | NSMouseExitedMask);
180     NSEventMask autocloseMask = NSLeftMouseDownMask | NSRightMouseDownMask | NSKeyDownMask;
181     [bubble setAutomaticallyCloseWithMask:autocloseMask filterMask:filterMask block:[bubble] {
182         [bubble close];
183     }];
184     [bubble showPreviewItem:[NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()] itemFrame:itemFrame];
185 }
186
187 #pragma mark Image actions
188
189 - (NSArray *)_defaultMenuItemsForImage
190 {
191     RetainPtr<NSMenuItem> copyImageItem = [self _createActionMenuItemForTag:kWKContextActionItemTagCopyImage];
192     RetainPtr<NSMenuItem> addToPhotosItem;
193     if ([self _canAddImageToPhotos])
194         addToPhotosItem = [self _createActionMenuItemForTag:kWKContextActionItemTagAddImageToPhotos];
195     else
196         addToPhotosItem = [NSMenuItem separatorItem];
197     RetainPtr<NSMenuItem> saveToDownloadsItem = [self _createActionMenuItemForTag:kWKContextActionItemTagSaveImageToDownloads];
198     RetainPtr<NSMenuItem> shareItem = [self _createActionMenuItemForTag:kWKContextActionItemTagShareImage];
199
200     if (RefPtr<ShareableBitmap> bitmap = _hitTestResult.image) {
201         RetainPtr<CGImageRef> image = bitmap->makeCGImage();
202         RetainPtr<NSImage> nsImage = adoptNS([[NSImage alloc] initWithCGImage:image.get() size:NSZeroSize]);
203         _sharingServicePicker = adoptNS([[NSSharingServicePicker alloc] initWithItems:@[ nsImage.get() ]]);
204         [_sharingServicePicker setDelegate:self];
205         [shareItem setSubmenu:[_sharingServicePicker menu]];
206     }
207
208     return @[ copyImageItem.get(), addToPhotosItem.get(), saveToDownloadsItem.get(), shareItem.get() ];
209 }
210
211 - (void)_copyImage:(id)sender
212 {
213     RefPtr<ShareableBitmap> bitmap = _hitTestResult.image;
214     if (!bitmap)
215         return;
216
217     RetainPtr<CGImageRef> image = bitmap->makeCGImage();
218     RetainPtr<NSImage> nsImage = adoptNS([[NSImage alloc] initWithCGImage:image.get() size:NSZeroSize]);
219     [[NSPasteboard generalPasteboard] clearContents];
220     [[NSPasteboard generalPasteboard] writeObjects:@[ nsImage.get() ]];
221 }
222
223 - (void)_saveImageToDownloads:(id)sender
224 {
225     WebHitTestResult* hitTestResult = _page->lastMouseMoveHitTestResult();
226     _page->process().context().download(_page, hitTestResult->absoluteImageURL());
227 }
228
229 // FIXME: We should try to share this with WebPageProxyMac's similar PDF functions.
230 static NSString *temporaryPhotosDirectoryPath()
231 {
232     static NSString *temporaryPhotosDirectoryPath;
233
234     if (!temporaryPhotosDirectoryPath) {
235         NSString *temporaryDirectoryTemplate = [NSTemporaryDirectory() stringByAppendingPathComponent:@"WebKitPhotos-XXXXXX"];
236         CString templateRepresentation = [temporaryDirectoryTemplate fileSystemRepresentation];
237
238         if (mkdtemp(templateRepresentation.mutableData()))
239             temporaryPhotosDirectoryPath = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:templateRepresentation.data() length:templateRepresentation.length()] copy];
240     }
241
242     return temporaryPhotosDirectoryPath;
243 }
244
245 static NSString *pathToPhotoOnDisk(NSString *suggestedFilename)
246 {
247     NSString *photoDirectoryPath = temporaryPhotosDirectoryPath();
248     if (!photoDirectoryPath) {
249         WTFLogAlways("Cannot create temporary photo download directory.");
250         return nil;
251     }
252
253     NSString *path = [photoDirectoryPath stringByAppendingPathComponent:suggestedFilename];
254
255     NSFileManager *fileManager = [NSFileManager defaultManager];
256     if ([fileManager fileExistsAtPath:path]) {
257         NSString *pathTemplatePrefix = [photoDirectoryPath stringByAppendingPathComponent:@"XXXXXX-"];
258         NSString *pathTemplate = [pathTemplatePrefix stringByAppendingString:suggestedFilename];
259         CString pathTemplateRepresentation = [pathTemplate fileSystemRepresentation];
260
261         int fd = mkstemps(pathTemplateRepresentation.mutableData(), pathTemplateRepresentation.length() - strlen([pathTemplatePrefix fileSystemRepresentation]) + 1);
262         if (fd < 0) {
263             WTFLogAlways("Cannot create photo file in the temporary directory (%@).", suggestedFilename);
264             return nil;
265         }
266
267         close(fd);
268         path = [fileManager stringWithFileSystemRepresentation:pathTemplateRepresentation.data() length:pathTemplateRepresentation.length()];
269     }
270
271     return path;
272 }
273
274 - (BOOL)_canAddImageToPhotos
275 {
276     return [getIKSlideshowClass() canExportToApplication:@"com.apple.Photos"];
277 }
278
279 - (void)_addImageToPhotos:(id)sender
280 {
281     if (![self _canAddImageToPhotos])
282         return;
283
284     RefPtr<ShareableBitmap> bitmap = _hitTestResult.image;
285     if (!bitmap)
286         return;
287     RetainPtr<CGImageRef> image = bitmap->makeCGImage();
288
289     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
290         NSString * const suggestedFilename = @"image.jpg";
291
292         NSString *filePath = pathToPhotoOnDisk(suggestedFilename);
293         if (!filePath)
294             return;
295
296         NSURL *fileURL = [NSURL fileURLWithPath:filePath];
297         auto dest = adoptCF(CGImageDestinationCreateWithURL((CFURLRef)fileURL, kUTTypeJPEG, 1, nullptr));
298         CGImageDestinationAddImage(dest.get(), image.get(), nullptr);
299         CGImageDestinationFinalize(dest.get());
300
301         dispatch_async(dispatch_get_main_queue(), ^{
302             // This API provides no way to report failure, but if 18420778 is fixed so that it does, we should handle this.
303             [getIKSlideshowClass() exportSlideshowItem:filePath toApplication:@"com.apple.Photos"];
304         });
305     });
306 }
307
308 #pragma mark Text actions
309
310 - (NSArray *)_defaultMenuItemsForText
311 {
312     RetainPtr<NSMenuItem> copyTextItem = [self _createActionMenuItemForTag:kWKContextActionItemTagCopyText];
313     RetainPtr<NSMenuItem> lookupTextItem = [self _createActionMenuItemForTag:kWKContextActionItemTagLookupText];
314
315     return @[ copyTextItem.get(), lookupTextItem.get() ];
316 }
317
318 -(void)_copyText:(id)sender
319 {
320     _page->executeEditCommand("copy");
321 }
322
323 -(void)_lookupText:(id)sender
324 {
325     _page->performDictionaryLookupOfCurrentSelection();
326 }
327
328 #pragma mark NSMenuDelegate implementation
329
330 - (void)menuNeedsUpdate:(NSMenu *)menu
331 {
332     if (menu != _wkView.actionMenu)
333         return;
334
335     ASSERT(_state != ActionMenuState::None);
336
337     // FIXME: We need to be able to cancel this if the menu goes away.
338     // FIXME: Connection can be null if the process is closed; we should clean up better in that case.
339     if (_state == ActionMenuState::Pending) {
340         if (auto* connection = _page->process().connection())
341             connection->waitForAndDispatchImmediately<Messages::WebPageProxy::DidPerformActionMenuHitTest>(_page->pageID(), std::chrono::milliseconds(500));
342     }
343
344     [self _updateActionMenuItems];
345 }
346
347 #pragma mark NSSharingServicePickerDelegate implementation
348
349 - (NSArray *)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker sharingServicesForItems:(NSArray *)items mask:(NSSharingServiceMask)mask proposedSharingServices:(NSArray *)proposedServices
350 {
351     RetainPtr<NSMutableArray> services = adoptNS([[NSMutableArray alloc] initWithCapacity:proposedServices.count]);
352
353     for (NSSharingService *service in proposedServices) {
354         if ([service.name isEqualToString:NSSharingServiceNameAddToIPhoto])
355             continue;
356         [services addObject:service];
357     }
358
359     return services.autorelease();
360 }
361
362 - (id <NSSharingServiceDelegate>)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker delegateForSharingService:(NSSharingService *)sharingService
363 {
364     return self;
365 }
366
367 #pragma mark NSSharingServiceDelegate implementation
368
369 - (NSWindow *)sharingService:(NSSharingService *)sharingService sourceWindowForShareItems:(NSArray *)items sharingContentScope:(NSSharingContentScope *)sharingContentScope
370 {
371     return _wkView.window;
372 }
373
374 #pragma mark Menu Items
375
376 - (RetainPtr<NSMenuItem>)_createActionMenuItemForTag:(uint32_t)tag
377 {
378     SEL selector = nullptr;
379     NSString *title = nil;
380     NSImage *image = nil;
381
382     // FIXME: These titles need to be localized.
383     switch (tag) {
384     case kWKContextActionItemTagOpenLinkInDefaultBrowser:
385         selector = @selector(_openURLFromActionMenu:);
386         title = @"Open";
387         image = webKitBundleImageNamed(@"OpenInNewWindowTemplate");
388         break;
389
390     case kWKContextActionItemTagPreviewLink:
391         selector = @selector(_quickLookURLFromActionMenu:);
392         title = @"Preview";
393         image = [NSImage imageNamed:NSImageNameQuickLookTemplate];
394         break;
395
396     case kWKContextActionItemTagAddLinkToSafariReadingList:
397         selector = @selector(_addToReadingListFromActionMenu:);
398         title = @"Add to Safari Reading List";
399         image = [NSImage imageNamed:NSImageNameBookmarksTemplate];
400         break;
401
402     case kWKContextActionItemTagCopyImage:
403         selector = @selector(_copyImage:);
404         title = @"Copy";
405         image = webKitBundleImageNamed(@"CopyImageTemplate");
406         break;
407
408     case kWKContextActionItemTagAddImageToPhotos:
409         selector = @selector(_addImageToPhotos:);
410         title = @"Add to Photos";
411         image = webKitBundleImageNamed(@"AddImageToPhotosTemplate");
412         break;
413
414     case kWKContextActionItemTagSaveImageToDownloads:
415         selector = @selector(_saveImageToDownloads:);
416         title = @"Save to Downloads";
417         image = webKitBundleImageNamed(@"SaveImageToDownloadsTemplate");
418         break;
419
420     case kWKContextActionItemTagShareImage:
421         title = @"Share";
422         image = webKitBundleImageNamed(@"ShareImageTemplate");
423         break;
424
425     case kWKContextActionItemTagCopyText:
426         selector = @selector(_copyText:);
427         title = @"Copy";
428         image = [NSImage imageNamed:@"NSActionMenuCopy"];
429         break;
430
431     case kWKContextActionItemTagLookupText:
432         selector = @selector(_lookupText:);
433         title = @"Lookup";
434         image = [NSImage imageNamed:@"NSActionMenuLookup"];
435         break;
436
437     default:
438         ASSERT_NOT_REACHED();
439         return nil;
440     }
441
442     RetainPtr<NSMenuItem> item = adoptNS([[NSMenuItem alloc] initWithTitle:title action:selector keyEquivalent:@""]);
443     [item setImage:image];
444     [item setTarget:self];
445     [item setTag:tag];
446     return item;
447 }
448
449 static NSImage *webKitBundleImageNamed(NSString *name)
450 {
451     return [[NSBundle bundleForClass:[WKView class]] imageForResource:name];
452 }
453
454 - (NSArray *)_defaultMenuItems
455 {
456     if (WebHitTestResult* hitTestResult = _page->lastMouseMoveHitTestResult()) {
457         if (!hitTestResult->absoluteImageURL().isEmpty() && _hitTestResult.image) {
458             _type = kWKActionMenuImage;
459             return [self _defaultMenuItemsForImage];
460         }
461
462         if (!hitTestResult->absoluteLinkURL().isEmpty()) {
463             _type = kWKActionMenuLink;
464             return [self _defaultMenuItemsForLink];
465         }
466
467         if (hitTestResult->isTextNode()) {
468             if (DDActionContext *actionContext = _hitTestResult.actionContext.get()) {
469                 WKSetDDActionContextIsForActionMenu(actionContext);
470                 actionContext.highlightFrame = [_wkView.window convertRectToScreen:[_wkView convertRect:_hitTestResult.actionBoundingBox toView:nil]];
471                 NSArray *dataDetectorMenuItems = [[getDDActionsManagerClass() sharedManager] menuItemsForResult:[_hitTestResult.actionContext mainResult] actionContext:actionContext];
472                 if (dataDetectorMenuItems.count) {
473                     _type = kWKActionMenuDataDetectedItem;
474                     return dataDetectorMenuItems;
475                 }
476             }
477             _type = kWKActionMenuReadOnlyText;
478             return [self _defaultMenuItemsForText];
479         }
480     }
481
482     _type = kWKActionMenuNone;
483     return _state != ActionMenuState::Ready ? @[ [NSMenuItem separatorItem] ] : @[ ];
484 }
485
486 - (void)_updateActionMenuItems
487 {
488     [_wkView.actionMenu removeAllItems];
489
490     NSArray *menuItems = [self _defaultMenuItems];
491     if ([_wkView respondsToSelector:@selector(_actionMenuItemsForHitTestResult:defaultActionMenuItems:)])
492         menuItems = [_wkView _actionMenuItemsForHitTestResult:toAPI(_page->lastMouseMoveHitTestResult()) defaultActionMenuItems:menuItems];
493     else
494         menuItems = [_wkView _actionMenuItemsForHitTestResult:toAPI(_page->lastMouseMoveHitTestResult()) withType:_type defaultActionMenuItems:menuItems];
495
496     for (NSMenuItem *item in menuItems)
497         [_wkView.actionMenu addItem:item];
498 }
499
500 @end
501
502 #endif // PLATFORM(MAC)