2 * Copyright (C) 2014 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
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.
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.
27 #import "WKActionMenuController.h"
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>
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;
58 SOFT_LINK_FRAMEWORK_IN_UMBRELLA(Quartz, ImageKit)
59 SOFT_LINK_CLASS(ImageKit, IKSlideshow)
61 using namespace WebCore;
62 using namespace WebKit;
64 enum class ActionMenuState {
70 @interface WKActionMenuController ()
71 - (void)_updateActionMenuItems;
74 @implementation WKActionMenuController {
78 ActionMenuState _state;
79 ActionMenuHitTestResult _hitTestResult;
80 RetainPtr<NSSharingServicePicker> _sharingServicePicker;
83 - (instancetype)initWithPage:(WebPageProxy&)page view:(WKView *)wkView
96 - (void)willDestroyView:(WKView *)view
102 - (void)prepareForMenu:(NSMenu *)menu withEvent:(NSEvent *)event
104 if (menu != _wkView.actionMenu)
107 [self _updateActionMenuItems];
109 _page->performActionMenuHitTestAtLocation([_wkView convertPoint:event.locationInWindow fromView:nil]);
111 _state = ActionMenuState::Pending;
114 - (void)willOpenMenu:(NSMenu *)menu withEvent:(NSEvent *)event
116 if (menu != _wkView.actionMenu)
119 ASSERT(_state != ActionMenuState::None);
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));
128 if (_state == ActionMenuState::Ready)
129 [self _updateActionMenuItems];
132 - (void)didCloseMenu:(NSMenu *)menu withEvent:(NSEvent *)event
134 if (menu != _wkView.actionMenu)
137 _state = ActionMenuState::None;
138 _hitTestResult = ActionMenuHitTestResult();
139 _sharingServicePicker = nil;
142 - (void)didPerformActionMenuHitTest:(const ActionMenuHitTestResult&)hitTestResult
144 // FIXME: This needs to use the WebKit2 callback mechanism to avoid out-of-order replies.
145 _state = ActionMenuState::Ready;
146 _hitTestResult = hitTestResult;
149 #pragma mark Link actions
151 - (NSArray *)_defaultMenuItemsForLink
153 WebHitTestResult* hitTestResult = _page->activeActionMenuHitTestResult();
154 if (!WebCore::protocolIsInHTTPFamily(hitTestResult->absoluteLinkURL()))
157 RetainPtr<NSMenuItem> openLinkItem = [self _createActionMenuItemForTag:kWKContextActionItemTagOpenLinkInDefaultBrowser];
158 RetainPtr<NSMenuItem> previewLinkItem = [self _createActionMenuItemForTag:kWKContextActionItemTagPreviewLink];
159 RetainPtr<NSMenuItem> readingListItem = [self _createActionMenuItemForTag:kWKContextActionItemTagAddLinkToSafariReadingList];
161 // FIXME: The separator item is required to work around <rdar://18684207>.
162 return @[ openLinkItem.get(), previewLinkItem.get(), [NSMenuItem separatorItem], readingListItem.get() ];
165 - (void)_openURLFromActionMenu:(id)sender
167 WebHitTestResult* hitTestResult = _page->activeActionMenuHitTestResult();
168 [[NSWorkspace sharedWorkspace] openURL:[NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()]];
171 - (void)_addToReadingListFromActionMenu:(id)sender
173 WebHitTestResult* hitTestResult = _page->activeActionMenuHitTestResult();
174 NSSharingService *service = [NSSharingService sharingServiceNamed:NSSharingServiceNameAddToSafariReadingList];
175 [service performWithItems:@[ [NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()] ]];
178 - (void)_quickLookURLFromActionMenu:(id)sender
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);
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] {
194 [bubble showPreviewItem:[NSURL _web_URLWithWTFString:hitTestResult->absoluteLinkURL()] itemFrame:itemFrame];
197 #pragma mark Image actions
199 - (NSArray *)_defaultMenuItemsForImage
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];
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]];
213 return @[ copyImageItem.get(), addToPhotosItem.get(), saveToDownloadsItem.get(), shareItem.get() ];
216 - (void)_copyImage:(id)sender
218 RefPtr<ShareableBitmap> bitmap = _hitTestResult.image;
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() ]];
228 - (void)_saveImageToDownloads:(id)sender
230 WebHitTestResult* hitTestResult = _page->activeActionMenuHitTestResult();
231 _page->process().context().download(_page, hitTestResult->absoluteImageURL());
234 // FIXME: We should try to share this with WebPageProxyMac's similar PDF functions.
235 static NSString *temporaryPhotosDirectoryPath()
237 static NSString *temporaryPhotosDirectoryPath;
239 if (!temporaryPhotosDirectoryPath) {
240 NSString *temporaryDirectoryTemplate = [NSTemporaryDirectory() stringByAppendingPathComponent:@"WebKitPhotos-XXXXXX"];
241 CString templateRepresentation = [temporaryDirectoryTemplate fileSystemRepresentation];
243 if (mkdtemp(templateRepresentation.mutableData()))
244 temporaryPhotosDirectoryPath = [[[NSFileManager defaultManager] stringWithFileSystemRepresentation:templateRepresentation.data() length:templateRepresentation.length()] copy];
247 return temporaryPhotosDirectoryPath;
250 static NSString *pathToPhotoOnDisk(NSString *suggestedFilename)
252 NSString *photoDirectoryPath = temporaryPhotosDirectoryPath();
253 if (!photoDirectoryPath) {
254 WTFLogAlways("Cannot create temporary photo download directory.");
258 NSString *path = [photoDirectoryPath stringByAppendingPathComponent:suggestedFilename];
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];
266 int fd = mkstemps(pathTemplateRepresentation.mutableData(), pathTemplateRepresentation.length() - strlen([pathTemplatePrefix fileSystemRepresentation]) + 1);
268 WTFLogAlways("Cannot create photo file in the temporary directory (%@).", suggestedFilename);
273 path = [fileManager stringWithFileSystemRepresentation:pathTemplateRepresentation.data() length:pathTemplateRepresentation.length()];
279 - (void)_addImageToPhotos:(id)sender
281 // FIXME: We shouldn't even add the menu item if this is the case, for now.
282 if (![getIKSlideshowClass() canExportToApplication:@"com.apple.Photos"])
285 RefPtr<ShareableBitmap> bitmap = _hitTestResult.image;
288 RetainPtr<CGImageRef> image = bitmap->makeCGImage();
290 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
291 NSString * const suggestedFilename = @"image.jpg";
293 NSString *filePath = pathToPhotoOnDisk(suggestedFilename);
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());
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"];
309 #pragma mark Menu Items
311 - (RetainPtr<NSMenuItem>)_createActionMenuItemForTag:(uint32_t)tag
313 SEL selector = nullptr;
314 NSString *title = nil;
315 NSImage *image = nil;
317 // FIXME: These titles need to be localized.
319 case kWKContextActionItemTagOpenLinkInDefaultBrowser:
320 selector = @selector(_openURLFromActionMenu:);
322 image = webKitBundleImageNamed(@"OpenInNewWindowTemplate");
325 case kWKContextActionItemTagPreviewLink:
326 selector = @selector(_quickLookURLFromActionMenu:);
328 image = [NSImage imageNamed:NSImageNameQuickLookTemplate];
331 case kWKContextActionItemTagAddLinkToSafariReadingList:
332 selector = @selector(_addToReadingListFromActionMenu:);
333 title = @"Add to Safari Reading List";
334 image = [NSImage imageNamed:NSImageNameBookmarksTemplate];
337 case kWKContextActionItemTagCopyImage:
338 selector = @selector(_copyImage:);
340 image = webKitBundleImageNamed(@"CopyImageTemplate");
343 case kWKContextActionItemTagAddImageToPhotos:
344 selector = @selector(_addImageToPhotos:);
345 title = @"Add to Photos";
346 image = webKitBundleImageNamed(@"AddImageToPhotosTemplate");
349 case kWKContextActionItemTagSaveImageToDownloads:
350 selector = @selector(_saveImageToDownloads:);
351 title = @"Save to Downloads";
352 image = webKitBundleImageNamed(@"SaveImageToDownloadsTemplate");
355 case kWKContextActionItemTagShareImage:
357 image = webKitBundleImageNamed(@"ShareImageTemplate");
361 ASSERT_NOT_REACHED();
365 RetainPtr<NSMenuItem> item = adoptNS([[NSMenuItem alloc] initWithTitle:title action:selector keyEquivalent:@""]);
366 [item setImage:image];
367 [item setTarget:self];
372 static NSImage *webKitBundleImageNamed(NSString *name)
374 return [[NSBundle bundleForClass:[WKView class]] imageForResource:name];
377 - (NSArray *)_defaultMenuItems
379 if (WebHitTestResult* hitTestResult = _page->activeActionMenuHitTestResult()) {
380 if (!hitTestResult->absoluteImageURL().isEmpty())
381 return [self _defaultMenuItemsForImage];
382 if (!hitTestResult->absoluteLinkURL().isEmpty())
383 return [self _defaultMenuItemsForLink];
389 - (void)_updateActionMenuItems
391 [_wkView.actionMenu removeAllItems];
393 NSArray *menuItems = [_wkView _actionMenuItemsForHitTestResult:toAPI(_page->activeActionMenuHitTestResult()) defaultActionMenuItems:[self _defaultMenuItems]];
395 for (NSMenuItem *item in menuItems)
396 [_wkView.actionMenu addItem:item];
401 #endif // PLATFORM(MAC)