[Mac][WebKit2] Move action menu code into its own file
[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 "ActionMenuHitTestResult.h"
32 #import "WKActionMenuItemTypes.h"
33 #import "WKNSURLExtras.h"
34 #import "WKViewInternal.h"
35 #import "WebContext.h"
36 #import "WebPageMessages.h"
37 #import "WebPageProxy.h"
38 #import "WebPageProxyMessages.h"
39 #import "WebProcessProxy.h"
40 #import <ImageIO/ImageIO.h>
41 #import <ImageKit/ImageKit.h>
42 #import <WebCore/NSSharingServicePickerSPI.h>
43 #import <WebCore/NSViewSPI.h>
44 #import <WebCore/SoftLinking.h>
45 #import <WebCore/URL.h>
46
47 // FIXME: This should move into an SPI header if it stays.
48 @class QLPreviewBubble;
49 @interface NSObject (WKQLPreviewBubbleDetails)
50 @property (copy) NSArray * controls;
51 @property NSSize maximumSize;
52 @property NSRectEdge preferredEdge;
53 @property (retain) IBOutlet NSWindow* parentWindow;
54 - (void)showPreviewItem:(id)previewItem itemFrame:(NSRect)frame;
55 - (void)setAutomaticallyCloseWithMask:(NSEventMask)autocloseMask filterMask:(NSEventMask)filterMask block:(void (^)(void))block;
56 @end
57
58 SOFT_LINK_FRAMEWORK_IN_UMBRELLA(Quartz, ImageKit)
59 SOFT_LINK_CLASS(ImageKit, IKSlideshow)
60
61 using namespace WebCore;
62 using namespace WebKit;
63
64 enum class ActionMenuState {
65     None = 0,
66     Pending,
67     Ready
68 };
69
70 @interface WKActionMenuController ()
71 - (void)_updateActionMenuItems;
72 @end
73
74 @implementation WKActionMenuController {
75     WebPageProxy *_page;
76     WKView *_wkView;
77
78     ActionMenuState _state;
79     ActionMenuHitTestResult _hitTestResult;
80     RetainPtr<NSSharingServicePicker> _sharingServicePicker;
81 }
82
83 - (instancetype)initWithPage:(WebPageProxy&)page view:(WKView *)wkView
84 {
85     self = [super init];
86
87     if (!self)
88         return nil;
89
90     _page = &page;
91     _wkView = wkView;
92
93     return self;
94 }
95
96 - (void)willDestroyView:(WKView *)view
97 {
98     _page = nullptr;
99     _wkView = nullptr;
100 }
101
102 - (void)prepareForMenu:(NSMenu *)menu withEvent:(NSEvent *)event
103 {
104     if (menu != _wkView.actionMenu)
105         return;
106
107     [self _updateActionMenuItems];
108
109     _page->performActionMenuHitTestAtLocation([_wkView convertPoint:event.locationInWindow fromView:nil]);
110
111     _state = ActionMenuState::Pending;
112 }
113
114 - (void)willOpenMenu:(NSMenu *)menu withEvent:(NSEvent *)event
115 {
116     if (menu != _wkView.actionMenu)
117         return;
118
119     ASSERT(_state != ActionMenuState::None);
120
121     // FIXME: We need to be able to cancel this if the menu goes away.
122     // FIXME: Connection can be null if the process is closed; we should clean up better in that case.
123     if (_state == ActionMenuState::Pending) {
124         if (auto* connection = _page->process().connection())
125             connection->waitForAndDispatchImmediately<Messages::WebPageProxy::DidPerformActionMenuHitTest>(_page->pageID(), std::chrono::milliseconds(500));
126     }
127
128     if (_state == ActionMenuState::Ready)
129         [self _updateActionMenuItems];
130 }
131
132 - (void)didCloseMenu:(NSMenu *)menu withEvent:(NSEvent *)event
133 {
134     if (menu != _wkView.actionMenu)
135         return;
136     
137     _state = ActionMenuState::None;
138     _hitTestResult = ActionMenuHitTestResult();
139     _sharingServicePicker = nil;
140 }
141
142 - (void)didPerformActionMenuHitTest:(const ActionMenuHitTestResult&)hitTestResult
143 {
144     // FIXME: This needs to use the WebKit2 callback mechanism to avoid out-of-order replies.
145     _state = ActionMenuState::Ready;
146     _hitTestResult = hitTestResult;
147 }
148
149 #pragma mark Link actions
150
151 - (NSArray *)_defaultMenuItemsForLink
152 {
153     WebHitTestResult* hitTestResult = _page->activeActionMenuHitTestResult();
154     if (!WebCore::protocolIsInHTTPFamily(hitTestResult->absoluteLinkURL()))
155         return @[ ];
156
157     RetainPtr<NSMenuItem> openLinkItem = [self _createActionMenuItemForTag:kWKContextActionItemTagOpenLinkInDefaultBrowser];
158     RetainPtr<NSMenuItem> previewLinkItem = [self _createActionMenuItemForTag:kWKContextActionItemTagPreviewLink];
159     RetainPtr<NSMenuItem> readingListItem = [self _createActionMenuItemForTag:kWKContextActionItemTagAddLinkToSafariReadingList];
160
161     // FIXME: The separator item is required to work around <rdar://18684207>.
162     return @[ openLinkItem.get(), previewLinkItem.get(), [NSMenuItem separatorItem], readingListItem.get() ];
163 }
164
165 - (void)_openURLFromActionMenu:(id)sender
166 {
167     WebHitTestResult* hitTestResult = _page->activeActionMenuHitTestResult();
168     [[NSWorkspace sharedWorkspace] openURL:[NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()]];
169 }
170
171 - (void)_addToReadingListFromActionMenu:(id)sender
172 {
173     WebHitTestResult* hitTestResult = _page->activeActionMenuHitTestResult();
174     NSSharingService *service = [NSSharingService sharingServiceNamed:NSSharingServiceNameAddToSafariReadingList];
175     [service performWithItems:@[ [NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()] ]];
176 }
177
178 - (void)_quickLookURLFromActionMenu:(id)sender
179 {
180     WebHitTestResult* hitTestResult = _page->activeActionMenuHitTestResult();
181     NSRect itemFrame = [_wkView convertRect:hitTestResult->elementBoundingBox() toView:nil];
182     NSSize maximumPreviewSize = NSMakeSize(_wkView.bounds.size.width * 0.75, _wkView.bounds.size.height * 0.75);
183
184     RetainPtr<QLPreviewBubble> bubble = adoptNS([[NSClassFromString(@"QLPreviewBubble") alloc] init]);
185     [bubble setParentWindow:_wkView.window];
186     [bubble setMaximumSize:maximumPreviewSize];
187     [bubble setPreferredEdge:NSMaxYEdge];
188     [bubble setControls:@[ ]];
189     NSEventMask filterMask = NSAnyEventMask & ~(NSAppKitDefinedMask | NSSystemDefinedMask | NSApplicationDefinedMask | NSMouseEnteredMask | NSMouseExitedMask);
190     NSEventMask autocloseMask = NSLeftMouseDownMask | NSRightMouseDownMask | NSKeyDownMask;
191     [bubble setAutomaticallyCloseWithMask:autocloseMask filterMask:filterMask block:[bubble] {
192         [bubble close];
193     }];
194     [bubble showPreviewItem:[NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()] itemFrame:itemFrame];
195 }
196
197 #pragma mark Image actions
198
199 - (NSArray *)_defaultMenuItemsForImage
200 {
201     RetainPtr<NSMenuItem> copyImageItem = [self _createActionMenuItemForTag:kWKContextActionItemTagCopyImage];
202     RetainPtr<NSMenuItem> addToPhotosItem = [self _createActionMenuItemForTag:kWKContextActionItemTagAddImageToPhotos];
203     RetainPtr<NSMenuItem> saveToDownloadsItem = [self _createActionMenuItemForTag:kWKContextActionItemTagSaveImageToDownloads];
204     RetainPtr<NSMenuItem> shareItem = [self _createActionMenuItemForTag:kWKContextActionItemTagShareImage];
205
206     if (RefPtr<ShareableBitmap> bitmap = _hitTestResult.image) {
207         RetainPtr<CGImageRef> image = bitmap->makeCGImage();
208         RetainPtr<NSImage> nsImage = adoptNS([[NSImage alloc] initWithCGImage:image.get() size:NSZeroSize]);
209         _sharingServicePicker = adoptNS([[NSSharingServicePicker alloc] initWithItems:@[ nsImage.get() ]]);
210         [shareItem setSubmenu:[_sharingServicePicker menu]];
211     }
212
213     return @[ copyImageItem.get(), addToPhotosItem.get(), saveToDownloadsItem.get(), shareItem.get() ];
214 }
215
216 - (void)_copyImage:(id)sender
217 {
218     RefPtr<ShareableBitmap> bitmap = _hitTestResult.image;
219     if (!bitmap)
220         return;
221
222     RetainPtr<CGImageRef> image = bitmap->makeCGImage();
223     RetainPtr<NSImage> nsImage = adoptNS([[NSImage alloc] initWithCGImage:image.get() size:NSZeroSize]);
224     [[NSPasteboard generalPasteboard] clearContents];
225     [[NSPasteboard generalPasteboard] writeObjects:@[ nsImage.get() ]];
226 }
227
228 - (void)_saveImageToDownloads:(id)sender
229 {
230     WebHitTestResult* hitTestResult = _page->activeActionMenuHitTestResult();
231     _page->process().context().download(_page, hitTestResult->absoluteImageURL());
232 }
233
234 // FIXME: We should try to share this with WebPageProxyMac's similar PDF functions.
235 static NSString *temporaryPhotosDirectoryPath()
236 {
237     static NSString *temporaryPhotosDirectoryPath;
238
239     if (!temporaryPhotosDirectoryPath) {
240         NSString *temporaryDirectoryTemplate = [NSTemporaryDirectory() stringByAppendingPathComponent:@"WebKitPhotos-XXXXXX"];
241         CString templateRepresentation = [temporaryDirectoryTemplate fileSystemRepresentation];
242
243         if (mkdtemp(templateRepresentation.mutableData()))
244             temporaryPhotosDirectoryPath = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:templateRepresentation.data() length:templateRepresentation.length()] copy];
245     }
246
247     return temporaryPhotosDirectoryPath;
248 }
249
250 static NSString *pathToPhotoOnDisk(NSString *suggestedFilename)
251 {
252     NSString *photoDirectoryPath = temporaryPhotosDirectoryPath();
253     if (!photoDirectoryPath) {
254         WTFLogAlways("Cannot create temporary photo download directory.");
255         return nil;
256     }
257
258     NSString *path = [photoDirectoryPath stringByAppendingPathComponent:suggestedFilename];
259
260     NSFileManager *fileManager = [NSFileManager defaultManager];
261     if ([fileManager fileExistsAtPath:path]) {
262         NSString *pathTemplatePrefix = [photoDirectoryPath stringByAppendingPathComponent:@"XXXXXX-"];
263         NSString *pathTemplate = [pathTemplatePrefix stringByAppendingString:suggestedFilename];
264         CString pathTemplateRepresentation = [pathTemplate fileSystemRepresentation];
265
266         int fd = mkstemps(pathTemplateRepresentation.mutableData(), pathTemplateRepresentation.length() - strlen([pathTemplatePrefix fileSystemRepresentation]) + 1);
267         if (fd < 0) {
268             WTFLogAlways("Cannot create photo file in the temporary directory (%@).", suggestedFilename);
269             return nil;
270         }
271
272         close(fd);
273         path = [fileManager stringWithFileSystemRepresentation:pathTemplateRepresentation.data() length:pathTemplateRepresentation.length()];
274     }
275
276     return path;
277 }
278
279 - (void)_addImageToPhotos:(id)sender
280 {
281     // FIXME: We shouldn't even add the menu item if this is the case, for now.
282     if (![getIKSlideshowClass() canExportToApplication:@"com.apple.Photos"])
283         return;
284
285     RefPtr<ShareableBitmap> bitmap = _hitTestResult.image;
286     if (!bitmap)
287         return;
288     RetainPtr<CGImageRef> image = bitmap->makeCGImage();
289
290     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
291         NSString * const suggestedFilename = @"image.jpg";
292
293         NSString *filePath = pathToPhotoOnDisk(suggestedFilename);
294         if (!filePath)
295             return;
296
297         NSURL *fileURL = [NSURL fileURLWithPath:filePath];
298         auto dest = adoptCF(CGImageDestinationCreateWithURL((CFURLRef)fileURL, kUTTypeJPEG, 1, nullptr));
299         CGImageDestinationAddImage(dest.get(), image.get(), nullptr);
300         CGImageDestinationFinalize(dest.get());
301
302         dispatch_async(dispatch_get_main_queue(), ^{
303             // This API provides no way to report failure, but if 18420778 is fixed so that it does, we should handle this.
304             [getIKSlideshowClass() exportSlideshowItem:filePath toApplication:@"com.apple.Photos"];
305         });
306     });
307 }
308
309 #pragma mark Menu Items
310
311 - (RetainPtr<NSMenuItem>)_createActionMenuItemForTag:(uint32_t)tag
312 {
313     SEL selector = nullptr;
314     NSString *title = nil;
315     NSImage *image = nil;
316
317     // FIXME: These titles need to be localized.
318     switch (tag) {
319     case kWKContextActionItemTagOpenLinkInDefaultBrowser:
320         selector = @selector(_openURLFromActionMenu:);
321         title = @"Open";
322         image = webKitBundleImageNamed(@"OpenInNewWindowTemplate");
323         break;
324
325     case kWKContextActionItemTagPreviewLink:
326         selector = @selector(_quickLookURLFromActionMenu:);
327         title = @"Preview";
328         image = [NSImage imageNamed:NSImageNameQuickLookTemplate];
329         break;
330
331     case kWKContextActionItemTagAddLinkToSafariReadingList:
332         selector = @selector(_addToReadingListFromActionMenu:);
333         title = @"Add to Safari Reading List";
334         image = [NSImage imageNamed:NSImageNameBookmarksTemplate];
335         break;
336
337     case kWKContextActionItemTagCopyImage:
338         selector = @selector(_copyImage:);
339         title = @"Copy";
340         image = webKitBundleImageNamed(@"CopyImageTemplate");
341         break;
342
343     case kWKContextActionItemTagAddImageToPhotos:
344         selector = @selector(_addImageToPhotos:);
345         title = @"Add to Photos";
346         image = webKitBundleImageNamed(@"AddImageToPhotosTemplate");
347         break;
348
349     case kWKContextActionItemTagSaveImageToDownloads:
350         selector = @selector(_saveImageToDownloads:);
351         title = @"Save to Downloads";
352         image = webKitBundleImageNamed(@"SaveImageToDownloadsTemplate");
353         break;
354
355     case kWKContextActionItemTagShareImage:
356         title = @"Share";
357         image = webKitBundleImageNamed(@"ShareImageTemplate");
358         break;
359
360     default:
361         ASSERT_NOT_REACHED();
362         return nil;
363     }
364
365     RetainPtr<NSMenuItem> item = adoptNS([[NSMenuItem alloc] initWithTitle:title action:selector keyEquivalent:@""]);
366     [item setImage:image];
367     [item setTarget:self];
368     [item setTag:tag];
369     return item;
370 }
371
372 static NSImage *webKitBundleImageNamed(NSString *name)
373 {
374     return [[NSBundle bundleForClass:[WKView class]] imageForResource:name];
375 }
376
377 - (NSArray *)_defaultMenuItems
378 {
379     if (WebHitTestResult* hitTestResult = _page->activeActionMenuHitTestResult()) {
380         if (!hitTestResult->absoluteImageURL().isEmpty())
381             return [self _defaultMenuItemsForImage];
382         if (!hitTestResult->absoluteLinkURL().isEmpty())
383             return [self _defaultMenuItemsForLink];
384     }
385
386     return @[ ];
387 }
388
389 - (void)_updateActionMenuItems
390 {
391     [_wkView.actionMenu removeAllItems];
392
393     NSArray *menuItems = [_wkView _actionMenuItemsForHitTestResult:toAPI(_page->activeActionMenuHitTestResult()) defaultActionMenuItems:[self _defaultMenuItems]];
394     
395     for (NSMenuItem *item in menuItems)
396         [_wkView.actionMenu addItem:item];
397 }
398
399 @end
400
401 #endif // PLATFORM(MAC)