0f35aab17952b2120d6bd8b1c18a705239b54030
[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)didCloseMenu:(NSMenu *)menu withEvent:(NSEvent *)event
108 {
109     if (menu != _wkView.actionMenu)
110         return;
111     
112     _state = ActionMenuState::None;
113     _hitTestResult = ActionMenuHitTestResult();
114     _type = kWKActionMenuNone;
115     _sharingServicePicker = nil;
116 }
117
118 - (void)didPerformActionMenuHitTest:(const ActionMenuHitTestResult&)hitTestResult
119 {
120     // FIXME: This needs to use the WebKit2 callback mechanism to avoid out-of-order replies.
121     _state = ActionMenuState::Ready;
122     _hitTestResult = hitTestResult;
123 }
124
125 #pragma mark Link actions
126
127 - (NSArray *)_defaultMenuItemsForLink
128 {
129     WebHitTestResult* hitTestResult = _page->lastMouseMoveHitTestResult();
130     if (!WebCore::protocolIsInHTTPFamily(hitTestResult->absoluteLinkURL()))
131         return @[ ];
132
133     RetainPtr<NSMenuItem> openLinkItem = [self _createActionMenuItemForTag:kWKContextActionItemTagOpenLinkInDefaultBrowser];
134     RetainPtr<NSMenuItem> previewLinkItem = [self _createActionMenuItemForTag:kWKContextActionItemTagPreviewLink];
135     RetainPtr<NSMenuItem> readingListItem = [self _createActionMenuItemForTag:kWKContextActionItemTagAddLinkToSafariReadingList];
136
137     return @[ openLinkItem.get(), previewLinkItem.get(), [NSMenuItem separatorItem], readingListItem.get() ];
138 }
139
140 - (void)_openURLFromActionMenu:(id)sender
141 {
142     WebHitTestResult* hitTestResult = _page->lastMouseMoveHitTestResult();
143     [[NSWorkspace sharedWorkspace] openURL:[NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()]];
144 }
145
146 - (void)_addToReadingListFromActionMenu:(id)sender
147 {
148     WebHitTestResult* hitTestResult = _page->lastMouseMoveHitTestResult();
149     NSSharingService *service = [NSSharingService sharingServiceNamed:NSSharingServiceNameAddToSafariReadingList];
150     [service performWithItems:@[ [NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()] ]];
151 }
152
153 - (void)_quickLookURLFromActionMenu:(id)sender
154 {
155     WebHitTestResult* hitTestResult = _page->lastMouseMoveHitTestResult();
156     NSRect itemFrame = [_wkView convertRect:hitTestResult->elementBoundingBox() toView:nil];
157     NSSize maximumPreviewSize = NSMakeSize(_wkView.bounds.size.width * 0.75, _wkView.bounds.size.height * 0.75);
158
159     RetainPtr<QLPreviewBubble> bubble = adoptNS([[NSClassFromString(@"QLPreviewBubble") alloc] init]);
160     [bubble setParentWindow:_wkView.window];
161     [bubble setMaximumSize:maximumPreviewSize];
162     [bubble setPreferredEdge:NSMaxYEdge];
163     [bubble setControls:@[ ]];
164     NSEventMask filterMask = NSAnyEventMask & ~(NSAppKitDefinedMask | NSSystemDefinedMask | NSApplicationDefinedMask | NSMouseEnteredMask | NSMouseExitedMask);
165     NSEventMask autocloseMask = NSLeftMouseDownMask | NSRightMouseDownMask | NSKeyDownMask;
166     [bubble setAutomaticallyCloseWithMask:autocloseMask filterMask:filterMask block:[bubble] {
167         [bubble close];
168     }];
169     [bubble showPreviewItem:[NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()] itemFrame:itemFrame];
170 }
171
172 #pragma mark Image actions
173
174 - (NSArray *)_defaultMenuItemsForImage
175 {
176     RetainPtr<NSMenuItem> copyImageItem = [self _createActionMenuItemForTag:kWKContextActionItemTagCopyImage];
177     RetainPtr<NSMenuItem> addToPhotosItem;
178     if ([self _canAddImageToPhotos])
179         addToPhotosItem = [self _createActionMenuItemForTag:kWKContextActionItemTagAddImageToPhotos];
180     else
181         addToPhotosItem = [NSMenuItem separatorItem];
182     RetainPtr<NSMenuItem> saveToDownloadsItem = [self _createActionMenuItemForTag:kWKContextActionItemTagSaveImageToDownloads];
183     RetainPtr<NSMenuItem> shareItem = [self _createActionMenuItemForTag:kWKContextActionItemTagShareImage];
184
185     if (RefPtr<ShareableBitmap> bitmap = _hitTestResult.image) {
186         RetainPtr<CGImageRef> image = bitmap->makeCGImage();
187         RetainPtr<NSImage> nsImage = adoptNS([[NSImage alloc] initWithCGImage:image.get() size:NSZeroSize]);
188         _sharingServicePicker = adoptNS([[NSSharingServicePicker alloc] initWithItems:@[ nsImage.get() ]]);
189         [_sharingServicePicker setDelegate:self];
190         [shareItem setSubmenu:[_sharingServicePicker menu]];
191     }
192
193     return @[ copyImageItem.get(), addToPhotosItem.get(), saveToDownloadsItem.get(), shareItem.get() ];
194 }
195
196 - (void)_copyImage:(id)sender
197 {
198     RefPtr<ShareableBitmap> bitmap = _hitTestResult.image;
199     if (!bitmap)
200         return;
201
202     RetainPtr<CGImageRef> image = bitmap->makeCGImage();
203     RetainPtr<NSImage> nsImage = adoptNS([[NSImage alloc] initWithCGImage:image.get() size:NSZeroSize]);
204     [[NSPasteboard generalPasteboard] clearContents];
205     [[NSPasteboard generalPasteboard] writeObjects:@[ nsImage.get() ]];
206 }
207
208 - (void)_saveImageToDownloads:(id)sender
209 {
210     WebHitTestResult* hitTestResult = _page->lastMouseMoveHitTestResult();
211     _page->process().context().download(_page, hitTestResult->absoluteImageURL());
212 }
213
214 // FIXME: We should try to share this with WebPageProxyMac's similar PDF functions.
215 static NSString *temporaryPhotosDirectoryPath()
216 {
217     static NSString *temporaryPhotosDirectoryPath;
218
219     if (!temporaryPhotosDirectoryPath) {
220         NSString *temporaryDirectoryTemplate = [NSTemporaryDirectory() stringByAppendingPathComponent:@"WebKitPhotos-XXXXXX"];
221         CString templateRepresentation = [temporaryDirectoryTemplate fileSystemRepresentation];
222
223         if (mkdtemp(templateRepresentation.mutableData()))
224             temporaryPhotosDirectoryPath = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:templateRepresentation.data() length:templateRepresentation.length()] copy];
225     }
226
227     return temporaryPhotosDirectoryPath;
228 }
229
230 static NSString *pathToPhotoOnDisk(NSString *suggestedFilename)
231 {
232     NSString *photoDirectoryPath = temporaryPhotosDirectoryPath();
233     if (!photoDirectoryPath) {
234         WTFLogAlways("Cannot create temporary photo download directory.");
235         return nil;
236     }
237
238     NSString *path = [photoDirectoryPath stringByAppendingPathComponent:suggestedFilename];
239
240     NSFileManager *fileManager = [NSFileManager defaultManager];
241     if ([fileManager fileExistsAtPath:path]) {
242         NSString *pathTemplatePrefix = [photoDirectoryPath stringByAppendingPathComponent:@"XXXXXX-"];
243         NSString *pathTemplate = [pathTemplatePrefix stringByAppendingString:suggestedFilename];
244         CString pathTemplateRepresentation = [pathTemplate fileSystemRepresentation];
245
246         int fd = mkstemps(pathTemplateRepresentation.mutableData(), pathTemplateRepresentation.length() - strlen([pathTemplatePrefix fileSystemRepresentation]) + 1);
247         if (fd < 0) {
248             WTFLogAlways("Cannot create photo file in the temporary directory (%@).", suggestedFilename);
249             return nil;
250         }
251
252         close(fd);
253         path = [fileManager stringWithFileSystemRepresentation:pathTemplateRepresentation.data() length:pathTemplateRepresentation.length()];
254     }
255
256     return path;
257 }
258
259 - (BOOL)_canAddImageToPhotos
260 {
261     return [getIKSlideshowClass() canExportToApplication:@"com.apple.Photos"];
262 }
263
264 - (void)_addImageToPhotos:(id)sender
265 {
266     if (![self _canAddImageToPhotos])
267         return;
268
269     RefPtr<ShareableBitmap> bitmap = _hitTestResult.image;
270     if (!bitmap)
271         return;
272     RetainPtr<CGImageRef> image = bitmap->makeCGImage();
273
274     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
275         NSString * const suggestedFilename = @"image.jpg";
276
277         NSString *filePath = pathToPhotoOnDisk(suggestedFilename);
278         if (!filePath)
279             return;
280
281         NSURL *fileURL = [NSURL fileURLWithPath:filePath];
282         auto dest = adoptCF(CGImageDestinationCreateWithURL((CFURLRef)fileURL, kUTTypeJPEG, 1, nullptr));
283         CGImageDestinationAddImage(dest.get(), image.get(), nullptr);
284         CGImageDestinationFinalize(dest.get());
285
286         dispatch_async(dispatch_get_main_queue(), ^{
287             // This API provides no way to report failure, but if 18420778 is fixed so that it does, we should handle this.
288             [getIKSlideshowClass() exportSlideshowItem:filePath toApplication:@"com.apple.Photos"];
289         });
290     });
291 }
292
293 #pragma mark NSMenuDelegate implementation
294
295 - (void)menuNeedsUpdate:(NSMenu *)menu
296 {
297     if (menu != _wkView.actionMenu)
298         return;
299
300     ASSERT(_state != ActionMenuState::None);
301
302     // FIXME: We need to be able to cancel this if the menu goes away.
303     // FIXME: Connection can be null if the process is closed; we should clean up better in that case.
304     if (_state == ActionMenuState::Pending) {
305         if (auto* connection = _page->process().connection())
306             connection->waitForAndDispatchImmediately<Messages::WebPageProxy::DidPerformActionMenuHitTest>(_page->pageID(), std::chrono::milliseconds(500));
307     }
308
309     [self _updateActionMenuItems];
310 }
311
312 #pragma mark NSSharingServicePickerDelegate implementation
313
314 - (id <NSSharingServiceDelegate>)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker delegateForSharingService:(NSSharingService *)sharingService
315 {
316     return self;
317 }
318
319 #pragma mark NSSharingServiceDelegate implementation
320
321 - (NSWindow *)sharingService:(NSSharingService *)sharingService sourceWindowForShareItems:(NSArray *)items sharingContentScope:(NSSharingContentScope *)sharingContentScope
322 {
323     return _wkView.window;
324 }
325
326 #pragma mark Menu Items
327
328 - (RetainPtr<NSMenuItem>)_createActionMenuItemForTag:(uint32_t)tag
329 {
330     SEL selector = nullptr;
331     NSString *title = nil;
332     NSImage *image = nil;
333
334     // FIXME: These titles need to be localized.
335     switch (tag) {
336     case kWKContextActionItemTagOpenLinkInDefaultBrowser:
337         selector = @selector(_openURLFromActionMenu:);
338         title = @"Open";
339         image = webKitBundleImageNamed(@"OpenInNewWindowTemplate");
340         break;
341
342     case kWKContextActionItemTagPreviewLink:
343         selector = @selector(_quickLookURLFromActionMenu:);
344         title = @"Preview";
345         image = [NSImage imageNamed:NSImageNameQuickLookTemplate];
346         break;
347
348     case kWKContextActionItemTagAddLinkToSafariReadingList:
349         selector = @selector(_addToReadingListFromActionMenu:);
350         title = @"Add to Safari Reading List";
351         image = [NSImage imageNamed:NSImageNameBookmarksTemplate];
352         break;
353
354     case kWKContextActionItemTagCopyImage:
355         selector = @selector(_copyImage:);
356         title = @"Copy";
357         image = webKitBundleImageNamed(@"CopyImageTemplate");
358         break;
359
360     case kWKContextActionItemTagAddImageToPhotos:
361         selector = @selector(_addImageToPhotos:);
362         title = @"Add to Photos";
363         image = webKitBundleImageNamed(@"AddImageToPhotosTemplate");
364         break;
365
366     case kWKContextActionItemTagSaveImageToDownloads:
367         selector = @selector(_saveImageToDownloads:);
368         title = @"Save to Downloads";
369         image = webKitBundleImageNamed(@"SaveImageToDownloadsTemplate");
370         break;
371
372     case kWKContextActionItemTagShareImage:
373         title = @"Share";
374         image = webKitBundleImageNamed(@"ShareImageTemplate");
375         break;
376
377     default:
378         ASSERT_NOT_REACHED();
379         return nil;
380     }
381
382     RetainPtr<NSMenuItem> item = adoptNS([[NSMenuItem alloc] initWithTitle:title action:selector keyEquivalent:@""]);
383     [item setImage:image];
384     [item setTarget:self];
385     [item setTag:tag];
386     return item;
387 }
388
389 static NSImage *webKitBundleImageNamed(NSString *name)
390 {
391     return [[NSBundle bundleForClass:[WKView class]] imageForResource:name];
392 }
393
394 - (NSArray *)_defaultMenuItems
395 {
396     if (WebHitTestResult* hitTestResult = _page->lastMouseMoveHitTestResult()) {
397         if (!hitTestResult->absoluteImageURL().isEmpty() && _hitTestResult.image) {
398             _type = kWKActionMenuImage;
399             return [self _defaultMenuItemsForImage];
400         }
401
402         if (!hitTestResult->absoluteLinkURL().isEmpty()) {
403             _type = kWKActionMenuLink;
404             return [self _defaultMenuItemsForLink];
405         }
406
407         if (hitTestResult->isTextNode()) {
408             if (DDActionContext *actionContext = _hitTestResult.actionContext.get()) {
409                 WKSetDDActionContextIsForActionMenu(actionContext);
410                 actionContext.highlightFrame = [_wkView.window convertRectToScreen:[_wkView convertRect:_hitTestResult.actionBoundingBox toView:nil]];
411                 NSArray *dataDetectorMenuItems = [[getDDActionsManagerClass() sharedManager] menuItemsForResult:[_hitTestResult.actionContext mainResult] actionContext:actionContext];
412                 if (dataDetectorMenuItems.count) {
413                     _type = kWKActionMenuDataDetectedItem;
414                     return dataDetectorMenuItems;
415                 }
416             }
417         }
418     }
419
420     _type = kWKActionMenuNone;
421     return _state != ActionMenuState::Ready ? @[ [NSMenuItem separatorItem] ] : @[ ];
422 }
423
424 - (void)_updateActionMenuItems
425 {
426     [_wkView.actionMenu removeAllItems];
427
428     NSArray *menuItems = [self _defaultMenuItems];
429     if ([_wkView respondsToSelector:@selector(_actionMenuItemsForHitTestResult:defaultActionMenuItems:)])
430         menuItems = [_wkView _actionMenuItemsForHitTestResult:toAPI(_page->lastMouseMoveHitTestResult()) defaultActionMenuItems:menuItems];
431     else
432         menuItems = [_wkView _actionMenuItemsForHitTestResult:toAPI(_page->lastMouseMoveHitTestResult()) withType:_type defaultActionMenuItems:menuItems];
433
434     for (NSMenuItem *item in menuItems)
435         [_wkView.actionMenu addItem:item];
436 }
437
438 @end
439
440 #endif // PLATFORM(MAC)