WKActionSheet.mm   [plain text]

/*
* 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 "WKActionSheet.h"

#if PLATFORM(IOS_FAMILY)

#import "UIKitSPI.h"
#import <wtf/RetainPtr.h>

@implementation WKActionSheet {
id <WKActionSheetDelegate> _sheetDelegate;
UIPopoverArrowDirection _arrowDirections;
BOOL _isRotating;
BOOL _readyToPresentAfterRotation;

WKActionSheetPresentationStyle _currentPresentationStyle;
RetainPtr<UIViewController> _currentPresentingViewController;
RetainPtr<UIViewController> _presentedViewControllerWhileRotating;
RetainPtr<id <UIPopoverPresentationControllerDelegate>> _popoverPresentationControllerDelegateWhileRotating;
}

- (id)init
{
self = [super init];
if (!self)
return nil;

_arrowDirections = UIPopoverArrowDirectionAny;

ALLOW_DEPRECATED_DECLARATIONS_BEGIN
if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPhone) {
// Only iPads support popovers that rotate. UIActionSheets actually block rotation on iPhone/iPod Touch
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self selector:@selector(willRotate) name:UIWindowWillRotateNotification object:nil];
[center addObserver:self selector:@selector(didRotate) name:UIWindowDidRotateNotification object:nil];
}
ALLOW_DEPRECATED_DECLARATIONS_END

return self;
}

- (void)dealloc
{
[self _cleanup];
[super dealloc];
}

- (void)_cleanup
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[NSObject cancelPreviousPerformRequestsWithTarget:self];
}

#pragma mark - Sheet presentation code

- (BOOL)presentSheet:(WKActionSheetPresentationStyle)style
{
// Calculate the presentation rect just before showing.
CGRect presentationRect = CGRectZero;
ALLOW_DEPRECATED_DECLARATIONS_BEGIN
if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPhone) {
presentationRect = [self _presentationRectForStyle:style];
if (CGRectIsEmpty(presentationRect))
return NO;
}
ALLOW_DEPRECATED_DECLARATIONS_END

_currentPresentationStyle = style;
return [self presentSheetFromRect:presentationRect];
}

- (CGRect)_presentationRectForStyle:(WKActionSheetPresentationStyle)style
{
if (style == WKActionSheetPresentAtElementRect)
return [_sheetDelegate presentationRectForIndicatedElement];

if (style == WKActionSheetPresentAtClosestIndicatorRect)
return [_sheetDelegate presentationRectForElementUsingClosestIndicatedRect];

return [_sheetDelegate initialPresentationRectInHostViewForSheet];
}

- (BOOL)presentSheetFromRect:(CGRect)presentationRect
{
UIView *view = [_sheetDelegate hostViewForSheet];
if (!view)
return NO;

UIViewController *presentedViewController = _presentedViewControllerWhileRotating.get() ? _presentedViewControllerWhileRotating.get() : self;
presentedViewController.modalPresentationStyle = UIModalPresentationPopover;

UIPopoverPresentationController *presentationController = presentedViewController.popoverPresentationController;
presentationController.sourceView = view;
presentationController.sourceRect = presentationRect;
presentationController.permittedArrowDirections = _arrowDirections;

if (_popoverPresentationControllerDelegateWhileRotating)
presentationController.delegate = _popoverPresentationControllerDelegateWhileRotating.get();

_currentPresentingViewController = [UIViewController _viewControllerForFullScreenPresentationFromView:view];
[_currentPresentingViewController presentViewController:presentedViewController animated:YES completion:nil];

return YES;
}

- (void)doneWithSheet:(BOOL)dismiss
{
if (dismiss) {
UIViewController *currentPresentedViewController = [_currentPresentingViewController presentedViewController];
if (currentPresentedViewController == self || currentPresentedViewController == _presentedViewControllerWhileRotating)
[currentPresentedViewController dismissViewControllerAnimated:YES completion:nil];
}

_currentPresentingViewController = nil;
_presentedViewControllerWhileRotating = nil;
_popoverPresentationControllerDelegateWhileRotating = nil;
_currentPresentationStyle = WKActionSheetPresentAtTouchLocation;

[self _cleanup];
}

#pragma mark - Rotation handling code

- (void)willRotate
{
// We want to save the view controller that is currently being presented to re-present it after rotation.
// Here are the various possible states that we have to handle:
// a) topViewController presenting ourselves (alertViewController) -> nominal case.
// There is no need to save the presented view controller, which is self.
// b) topViewController presenting ourselves presenting a content view controller ->
// This happens if one of the actions in the action sheet presented a different view controller inside the popover,
// using a current context presentation. This is for example the case with the Data Detectors action "Add to Contacts".
// c) topViewController presenting that content view controller directly.
// This happens if we were in the (b) case and then rotated the device. Since we dismiss the popover during the
// rotation, we take this opportunity to simplify the view controller hierarchy and simply re-present the content
// view controller, without re-presenting the alert controller.

UIView *view = [_sheetDelegate hostViewForSheet];
if (!view)
return;

UIViewController *presentingViewController = [UIViewController _viewControllerForFullScreenPresentationFromView:view];

// topPresentedViewController is either self (cases (a) and (b) above) or an action's view controller
// (case (c) above).
UIViewController *topPresentedViewController = [presentingViewController presentedViewController];

// We only have something to do if we're showing a popover (that we have to reposition).
// Otherwise the default UIAlertController behaviour is enough.
if ([topPresentedViewController presentationController].presentationStyle != UIModalPresentationPopover)
return;

if (_isRotating)
return;

_isRotating = YES;
_readyToPresentAfterRotation = NO;

UIViewController *presentedViewController = nil;
if ([self presentingViewController] != nil) {
// Handle cases (a) and (b) above (we (UIAlertController) are still in the presentation hierarchy).
// Save the view controller presented by one of the actions if there is one.
// (In the (a) case, presentedViewController will be nil).

presentedViewController = [self presentedViewController];
} else {
// Handle case (c) above.
// The view controller that we want to save is the top presented view controller, since we
// are not presenting it anymore.

presentedViewController = topPresentedViewController;
}

_presentedViewControllerWhileRotating = presentedViewController;

// Save the popover presentation controller's delegate, because in case (b) we're going to use
// a different popoverPresentationController after rotation to re-present the action view controller,
// and that action is still expecting delegate callbacks when the popover is dismissed.
_popoverPresentationControllerDelegateWhileRotating = [topPresentedViewController popoverPresentationController].delegate;

[presentingViewController dismissViewControllerAnimated:NO completion:^{
[self updateSheetPosition];
}];
}

- (void)updateSheetPosition
{
UIViewController *presentedViewController = _presentedViewControllerWhileRotating.get() ? _presentedViewControllerWhileRotating.get() : self;

// There are two asynchronous events which might trigger this call, and we have to wait for both of them before doing something.
// - One runloop iteration after rotation (to let the Web content re-layout, see below)
// - The completion of the view controller dismissal in willRotate.
// (We cannot present something again until the dismissal is done)

BOOL isBeingPresented = [presentedViewController presentingViewController] || [self presentingViewController];

if (_isRotating || !_readyToPresentAfterRotation || isBeingPresented)
return;

CGRect presentationRect = [self _presentationRectForStyle:_currentPresentationStyle];
BOOL wasPresentedViewControllerModal = [_presentedViewControllerWhileRotating isModalInPopover];

if (!CGRectIsEmpty(presentationRect) || wasPresentedViewControllerModal) {
// Re-present the popover only if we are still pointing to content onscreen, or if we can't dismiss it without losing information.
// (if the view controller is modal)

CGRect intersection = CGRectIntersection([[_sheetDelegate hostViewForSheet] bounds], presentationRect);
if (!CGRectIsEmpty(intersection))
[self presentSheetFromRect:intersection];
else if (wasPresentedViewControllerModal)
[self presentSheet:_currentPresentationStyle];

_presentedViewControllerWhileRotating = nil;
_popoverPresentationControllerDelegateWhileRotating = nil;
}
}

- (void)_didRotateAndLayout
{
_isRotating = NO;
_readyToPresentAfterRotation = YES;
[_sheetDelegate updatePositionInformation];
[self updateSheetPosition];
}

- (void)didRotate
{
// Handle the rotation on the next run loop interation as this
// allows the onOrientationChange event to fire, and the element node may
// be removed.
// <rdar://problem/9360929> Should re-present popover after layout rather than on the next runloop

[self performSelector:@selector(_didRotateAndLayout) withObject:nil afterDelay:0];
}

@end

#endif // PLATFORM(IOS_FAMILY)