/* * Copyright (C) 2014 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ #import "config.h" #import "WKActionSheetAssistant.h" #if PLATFORM(IOS) #import "APIUIClient.h" #import "TCCSPI.h" #import "UIKitSPI.h" #import "WKActionSheet.h" #import "WKContentViewInteraction.h" #import "WeakObjCPtr.h" #import "WebPageProxy.h" #import "WKNSURLExtras.h" #import "_WKActivatedElementInfoInternal.h" #import "_WKElementActionInternal.h" #import #import #import #import #import #if HAVE(SAFARI_SERVICES_FRAMEWORK) #import SOFT_LINK_FRAMEWORK(SafariServices) SOFT_LINK_CLASS(SafariServices, SSReadingList) #endif SOFT_LINK_PRIVATE_FRAMEWORK(TCC) SOFT_LINK(TCC, TCCAccessPreflight, TCCAccessPreflightResult, (CFStringRef service, CFDictionaryRef options), (service, options)) SOFT_LINK_CONSTANT(TCC, kTCCServicePhotos, CFStringRef) using namespace WebKit; @implementation WKActionSheetAssistant { WeakObjCPtr> _delegate; RetainPtr _interactionSheet; RetainPtr<_WKActivatedElementInfo> _elementInfo; UIView *_view; } - (id )delegate { return _delegate.getAutoreleased(); } - (void)setDelegate:(id )delegate { _delegate = delegate; } - (id)initWithView:(UIView *)view { _view = view; return self; } - (void)dealloc { [self cleanupSheet]; [super dealloc]; } - (UIView *)superviewForSheet { UIView *view = [_view window]; // FIXME: WebKit has a delegate to retrieve the superview for the image sheet (superviewForImageSheetForWebView) // Do we need it in WK2? // Find the top most view with a view controller UIViewController *controller = nil; UIView *currentView = _view; while (currentView) { UIViewController *aController = [UIViewController viewControllerForView:currentView]; if (aController) controller = aController; currentView = [currentView superview]; } if (controller) view = controller.view; return view; } - (CGRect)_presentationRectForSheetGivenPoint:(CGPoint)point inHostView:(UIView *)hostView { CGPoint presentationPoint = [hostView convertPoint:point fromView:_view]; CGRect presentationRect = CGRectMake(presentationPoint.x, presentationPoint.y, 1.0, 1.0); return CGRectInset(presentationRect, -22.0, -22.0); } - (UIView *)hostViewForSheet { return [self superviewForSheet]; } - (CGRect)initialPresentationRectInHostViewForSheet { UIView *view = [self superviewForSheet]; auto delegate = _delegate.get(); if (!view || !delegate) return CGRectZero; return [self _presentationRectForSheetGivenPoint:[delegate positionInformationForActionSheetAssistant:self].point inHostView:view]; } - (CGRect)presentationRectInHostViewForSheet { UIView *view = [self superviewForSheet]; auto delegate = _delegate.get(); if (!view || !delegate) return CGRectZero; const auto& positionInformation = [delegate positionInformationForActionSheetAssistant:self]; CGRect boundingRect = positionInformation.bounds; CGPoint fromPoint = positionInformation.point; // FIXME: We must adjust our presentation point to take into account a change in document scale. // Test to see if we are still within the target node as it may have moved after rotation. if (!CGRectContainsPoint(boundingRect, fromPoint)) fromPoint = CGPointMake(CGRectGetMidX(boundingRect), CGRectGetMidY(boundingRect)); return [self _presentationRectForSheetGivenPoint:fromPoint inHostView:view]; } - (void)updatePositionInformation { auto delegate = _delegate.get(); if ([delegate respondsToSelector:@selector(updatePositionInformationForActionSheetAssistant:)]) [delegate updatePositionInformationForActionSheetAssistant:self]; } - (BOOL)presentSheet { // Calculate the presentation rect just before showing. CGRect presentationRect = CGRectZero; if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPhone) { presentationRect = [self initialPresentationRectInHostViewForSheet]; if (CGRectIsEmpty(presentationRect)) return NO; } return [_interactionSheet presentSheetFromRect:presentationRect]; } - (void)updateSheetPosition { [_interactionSheet updateSheetPosition]; } - (void)_createSheetWithElementActions:(NSArray *)actions showLinkTitle:(BOOL)showLinkTitle { ASSERT(!_interactionSheet); auto delegate = _delegate.get(); if (!delegate) return; const auto& positionInformation = [delegate positionInformationForActionSheetAssistant:self]; NSURL *targetURL = [NSURL URLWithString:positionInformation.url]; NSString *urlScheme = [targetURL scheme]; BOOL isJavaScriptURL = [urlScheme length] && [urlScheme caseInsensitiveCompare:@"javascript"] == NSOrderedSame; // FIXME: We should check if Javascript is enabled in the preferences. _interactionSheet = adoptNS([[WKActionSheet alloc] init]); _interactionSheet.get().sheetDelegate = self; _interactionSheet.get().preferredStyle = UIAlertControllerStyleActionSheet; NSString *titleString = nil; BOOL titleIsURL = NO; if (showLinkTitle && [[targetURL absoluteString] length]) { if (isJavaScriptURL) titleString = WEB_UI_STRING_KEY("JavaScript", "JavaScript Action Sheet Title", "Title for action sheet for JavaScript link"); else { titleString = WebCore::userVisibleString(targetURL); titleIsURL = YES; } } else titleString = positionInformation.title; if ([titleString length]) { [_interactionSheet setTitle:titleString]; // We should configure the text field's line breaking mode correctly here, based on whether // the title is an URL or not, but the appropriate UIAlertController SPIs are not available yet. // The code that used to do this in the UIActionSheet world has been saved for reference in // Configure the UIAlertController's title appropriately. } for (_WKElementAction *action in actions) { [_interactionSheet _addActionWithTitle:[action title] style:UIAlertActionStyleDefault handler:^{ [action _runActionWithElementInfo:_elementInfo.get() forActionSheetAssistant:self]; [self cleanupSheet]; } shouldDismissHandler:^{ return (BOOL)(!action.dismissalHandler || action.dismissalHandler()); }]; } [_interactionSheet addAction:[UIAlertAction actionWithTitle:WEB_UI_STRING_KEY("Cancel", "Cancel button label in button bar", "Title for Cancel button label in button bar") style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { [self cleanupSheet]; }]]; if ([delegate respondsToSelector:@selector(actionSheetAssistant:willStartInteractionWithElement:)]) [delegate actionSheetAssistant:self willStartInteractionWithElement:_elementInfo.get()]; } - (void)showImageSheet { ASSERT(!_interactionSheet); ASSERT(!_elementInfo); auto delegate = _delegate.get(); if (!delegate) return; const auto& positionInformation = [delegate positionInformationForActionSheetAssistant:self]; NSURL *targetURL = [NSURL _web_URLWithWTFString:positionInformation.url]; auto defaultActions = [self defaultActionsForImageSheet]; auto elementInfo = adoptNS([[_WKActivatedElementInfo alloc] _initWithType:_WKActivatedElementTypeImage URL:targetURL location:positionInformation.point title:positionInformation.title rect:positionInformation.bounds image:positionInformation.image.get()]); RetainPtr actions = [delegate actionSheetAssistant:self decideActionsForElement:elementInfo.get() defaultActions:WTF::move(defaultActions)]; if (![actions count]) return; [self _createSheetWithElementActions:actions.get() showLinkTitle:YES]; if (!_interactionSheet) return; _elementInfo = WTF::move(elementInfo); if (![_interactionSheet presentSheet]) [self cleanupSheet]; } - (RetainPtr)defaultActionsForLinkSheet { auto delegate = _delegate.get(); if (!delegate) return nil; const auto& positionInformation = [delegate positionInformationForActionSheetAssistant:self]; NSURL *targetURL = [NSURL URLWithString:positionInformation.url]; if (!targetURL) return nil; auto defaultActions = adoptNS([[NSMutableArray alloc] init]); [defaultActions addObject:[_WKElementAction _elementActionWithType:_WKElementActionTypeOpen assistant:self]]; #if HAVE(SAFARI_SERVICES_FRAMEWORK) if ([getSSReadingListClass() supportsURL:targetURL]) [defaultActions addObject:[_WKElementAction _elementActionWithType:_WKElementActionTypeAddToReadingList assistant:self]]; #endif if (![[targetURL scheme] length] || [[targetURL scheme] caseInsensitiveCompare:@"javascript"] != NSOrderedSame) [defaultActions addObject:[_WKElementAction _elementActionWithType:_WKElementActionTypeCopy assistant:self]]; return defaultActions; } - (RetainPtr)defaultActionsForImageSheet { auto delegate = _delegate.get(); if (!delegate) return nil; const auto& positionInformation = [delegate positionInformationForActionSheetAssistant:self]; NSURL *targetURL = [NSURL _web_URLWithWTFString:positionInformation.url]; auto defaultActions = adoptNS([[NSMutableArray alloc] init]); if (!positionInformation.url.isEmpty()) [defaultActions addObject:[_WKElementAction _elementActionWithType:_WKElementActionTypeOpen assistant:self]]; #if HAVE(SAFARI_SERVICES_FRAMEWORK) if ([getSSReadingListClass() supportsURL:targetURL]) [defaultActions addObject:[_WKElementAction _elementActionWithType:_WKElementActionTypeAddToReadingList assistant:self]]; #endif if (TCCAccessPreflight(getkTCCServicePhotos(), NULL) != kTCCAccessPreflightDenied) [defaultActions addObject:[_WKElementAction _elementActionWithType:_WKElementActionTypeSaveImage assistant:self]]; if (!targetURL.scheme.length || [targetURL.scheme caseInsensitiveCompare:@"javascript"] != NSOrderedSame) [defaultActions addObject:[_WKElementAction _elementActionWithType:_WKElementActionTypeCopy assistant:self]]; return defaultActions; } - (void)showLinkSheet { ASSERT(!_interactionSheet); ASSERT(!_elementInfo); auto delegate = _delegate.get(); if (!delegate) return; const auto& positionInformation = [delegate positionInformationForActionSheetAssistant:self]; NSURL *targetURL = [NSURL _web_URLWithWTFString:positionInformation.url]; if (!targetURL) return; auto defaultActions = [self defaultActionsForLinkSheet]; RetainPtr<_WKActivatedElementInfo> elementInfo = adoptNS([[_WKActivatedElementInfo alloc] _initWithType:_WKActivatedElementTypeLink URL:targetURL location:positionInformation.point title:positionInformation.title rect:positionInformation.bounds image:positionInformation.image.get()]); RetainPtr actions = [delegate actionSheetAssistant:self decideActionsForElement:elementInfo.get() defaultActions:WTF::move(defaultActions)]; if (![actions count]) return; [self _createSheetWithElementActions:actions.get() showLinkTitle:YES]; if (!_interactionSheet) return; _elementInfo = WTF::move(elementInfo); if (![_interactionSheet presentSheet]) [self cleanupSheet]; } - (void)showDataDetectorsSheet { ASSERT(!_interactionSheet); auto delegate = _delegate.get(); if (!delegate) return; NSURL *targetURL = [NSURL URLWithString:[delegate positionInformationForActionSheetAssistant:self].url]; if (!targetURL) return; if (![[getDDDetectionControllerClass() tapAndHoldSchemes] containsObject:[targetURL scheme]]) return; NSArray *dataDetectorsActions = [[getDDDetectionControllerClass() sharedController] actionsForAnchor:nil url:targetURL forFrame:nil]; if ([dataDetectorsActions count] == 0) return; NSMutableArray *elementActions = [NSMutableArray array]; for (NSUInteger actionNumber = 0; actionNumber < [dataDetectorsActions count]; actionNumber++) { DDAction *action = [dataDetectorsActions objectAtIndex:actionNumber]; _WKElementAction *elementAction = [_WKElementAction elementActionWithTitle:[action localizedName] actionHandler:^(_WKActivatedElementInfo *actionInfo) { [[getDDDetectionControllerClass() sharedController] performAction:action fromAlertController:_interactionSheet.get() interactionDelegate:self]; }]; elementAction.dismissalHandler = ^{ return (BOOL)!action.hasUserInterface; }; [elementActions addObject:elementAction]; } [self _createSheetWithElementActions:elementActions showLinkTitle:NO]; if (!_interactionSheet) return; if (elementActions.count <= 1) _interactionSheet.get().arrowDirections = UIPopoverArrowDirectionUp | UIPopoverArrowDirectionDown; if (![_interactionSheet presentSheet]) [self cleanupSheet]; } - (void)cleanupSheet { auto delegate = _delegate.get(); if ([delegate respondsToSelector:@selector(actionSheetAssistantDidStopInteraction:)]) [delegate actionSheetAssistantDidStopInteraction:self]; [_interactionSheet doneWithSheet]; [_interactionSheet setSheetDelegate:nil]; _interactionSheet = nil; _elementInfo = nil; } @end #endif // PLATFORM(IOS)