blob: 4d7d1867bbbbc3ae825b9d51bb7f15485f05057f [file] [log] [blame]
/*
* Copyright (C) 2016 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"
#if PLATFORM(IOS_FAMILY)
#import "ValidationBubble.h"
#import <UIKit/UIGeometry.h>
#import <objc/message.h>
#import <pal/ios/UIKitSoftLink.h>
#import <pal/spi/ios/UIKitSPI.h>
#import <wtf/RetainPtr.h>
#import <wtf/SoftLinking.h>
#import <wtf/text/WTFString.h>
// Add a bit of vertical and horizontal padding between the
// label and its parent view, to avoid laying out the label
// against the edges of the popover view.
constexpr CGFloat validationBubbleHorizontalPadding = 17;
constexpr CGFloat validationBubbleVerticalPadding = 9;
// Avoid making the validation bubble too wide by enforcing a
// maximum width on the content size of the validation bubble
// view controller.
constexpr CGFloat validationBubbleMaxLabelWidth = 300;
// Avoid making the validation bubble too tall by truncating
// the label to a maximum of 4 lines.
constexpr NSInteger validationBubbleMaxNumberOfLines = 4;
@interface WebValidationBubbleViewController : UIViewController
@end
static const void* const validationBubbleViewControllerLabelKey = &validationBubbleViewControllerLabelKey;
static UILabel *label(WebValidationBubbleViewController *controller)
{
return objc_getAssociatedObject(controller, validationBubbleViewControllerLabelKey);
}
static void updateLabelFrame(WebValidationBubbleViewController *controller)
{
auto frameWithPadding = UIEdgeInsetsInsetRect(controller.view.bounds, controller.view.safeAreaInsets);
label(controller).frame = UIEdgeInsetsInsetRect(frameWithPadding, UIEdgeInsetsMake(validationBubbleVerticalPadding, validationBubbleHorizontalPadding, validationBubbleVerticalPadding, validationBubbleHorizontalPadding));
}
static void callSuper(WebValidationBubbleViewController *instance, SEL selector)
{
objc_super superStructure { instance, PAL::getUIViewControllerClass() };
auto msgSendSuper = reinterpret_cast<void(*)(objc_super*, SEL)>(objc_msgSendSuper);
msgSendSuper(&superStructure, selector);
}
static void WebValidationBubbleViewController_viewDidLoad(WebValidationBubbleViewController *instance, SEL)
{
callSuper(instance, @selector(viewDidLoad));
auto label = adoptNS([PAL::allocUILabelInstance() init]);
[label setFont:[PAL::getUIFontClass() preferredFontForTextStyle:PAL::get_UIKit_UIFontTextStyleCallout()]];
[label setLineBreakMode:NSLineBreakByTruncatingTail];
[label setNumberOfLines:validationBubbleMaxNumberOfLines];
[instance.view addSubview:label.get()];
objc_setAssociatedObject(instance, validationBubbleViewControllerLabelKey, label.get(), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
static void WebValidationBubbleViewController_viewWillLayoutSubviews(WebValidationBubbleViewController *instance, SEL)
{
callSuper(instance, @selector(viewWillLayoutSubviews));
updateLabelFrame(instance);
}
static void WebValidationBubbleViewController_viewSafeAreaInsetsDidChange(WebValidationBubbleViewController *instance, SEL)
{
callSuper(instance, @selector(viewSafeAreaInsetsDidChange));
updateLabelFrame(instance);
}
static WebValidationBubbleViewController *allocWebValidationBubbleViewControllerInstance()
{
static Class theClass = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
theClass = objc_allocateClassPair(PAL::getUIViewControllerClass(), "WebValidationBubbleViewController", 0);
class_addMethod(theClass, @selector(viewDidLoad), (IMP)WebValidationBubbleViewController_viewDidLoad, "v@:");
class_addMethod(theClass, @selector(viewWillLayoutSubviews), (IMP)WebValidationBubbleViewController_viewWillLayoutSubviews, "v@:");
class_addMethod(theClass, @selector(viewSafeAreaInsetsDidChange), (IMP)WebValidationBubbleViewController_viewSafeAreaInsetsDidChange, "v@:");
objc_registerClassPair(theClass);
});
return (WebValidationBubbleViewController *)[theClass alloc];
}
@interface WebValidationBubbleTapRecognizer : NSObject
@end
@implementation WebValidationBubbleTapRecognizer {
RetainPtr<UIViewController> _popoverController;
RetainPtr<UITapGestureRecognizer> _tapGestureRecognizer;
}
- (WebValidationBubbleTapRecognizer *)initWithPopoverController:(UIViewController *)popoverController
{
self = [super init];
if (!self)
return nil;
_popoverController = popoverController;
_tapGestureRecognizer = adoptNS([PAL::allocUITapGestureRecognizerInstance() initWithTarget:self action:@selector(dismissPopover)]);
[[_popoverController view] addGestureRecognizer:_tapGestureRecognizer.get()];
return self;
}
- (void)dealloc
{
[[_popoverController view] removeGestureRecognizer:_tapGestureRecognizer.get()];
[super dealloc];
}
- (void)dismissPopover
{
[_popoverController dismissViewControllerAnimated:NO completion:nil];
}
@end
@interface WebValidationBubbleDelegate : NSObject <UIPopoverPresentationControllerDelegate> {
}
@end
@implementation WebValidationBubbleDelegate
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller traitCollection:(UITraitCollection *)traitCollection
{
UNUSED_PARAM(controller);
UNUSED_PARAM(traitCollection);
// This is needed to force UIKit to use a popover on iPhone as well.
return UIModalPresentationNone;
}
@end
namespace WebCore {
ValidationBubble::ValidationBubble(UIView *view, const String& message, const Settings&)
: m_view(view)
, m_message(message)
{
m_popoverController = adoptNS([allocWebValidationBubbleViewControllerInstance() init]);
[m_popoverController setModalPresentationStyle:UIModalPresentationPopover];
m_tapRecognizer = adoptNS([[WebValidationBubbleTapRecognizer alloc] initWithPopoverController:m_popoverController.get()]);
UILabel *validationLabel = label(m_popoverController.get());
validationLabel.text = message;
m_fontSize = validationLabel.font.pointSize;
CGSize labelSize = [validationLabel sizeThatFits:CGSizeMake(validationBubbleMaxLabelWidth, CGFLOAT_MAX)];
[m_popoverController setPreferredContentSize:CGSizeMake(labelSize.width + validationBubbleHorizontalPadding * 2, labelSize.height + validationBubbleVerticalPadding * 2)];
}
ValidationBubble::~ValidationBubble()
{
[m_popoverController dismissViewControllerAnimated:NO completion:nil];
}
void ValidationBubble::show()
{
if ([m_popoverController parentViewController] || [m_popoverController presentingViewController])
return;
// Protect the validation bubble so it stays alive until it is effectively presented. UIKit does not deal nicely with
// dismissing a popover that is being presented.
RefPtr<ValidationBubble> protectedThis(this);
[m_presentingViewController presentViewController:m_popoverController.get() animated:NO completion:[protectedThis]() {
// Hide this popover from VoiceOver and instead announce the message.
[protectedThis->m_popoverController view].accessibilityElementsHidden = YES;
}];
PAL::softLinkUIKitUIAccessibilityPostNotification(PAL::get_UIKit_UIAccessibilityAnnouncementNotification(), m_message);
}
static UIViewController *fallbackViewController(UIView *view)
{
for (UIView *currentView = view; currentView; currentView = currentView.superview) {
if (UIViewController *viewController = [PAL::getUIViewControllerClass() viewControllerForView:currentView])
return viewController;
}
NSLog(@"Failed to find a view controller to show form validation popover");
return nil;
}
void ValidationBubble::setAnchorRect(const IntRect& anchorRect, UIViewController *presentingViewController)
{
if (!presentingViewController)
presentingViewController = fallbackViewController(m_view);
if (!presentingViewController)
return;
UIPopoverPresentationController *presentationController = [m_popoverController popoverPresentationController];
m_popoverDelegate = adoptNS([[WebValidationBubbleDelegate alloc] init]);
presentationController.delegate = m_popoverDelegate.get();
presentationController.passthroughViews = @[ presentingViewController.view, m_view ];
presentationController.sourceView = m_view;
presentationController.sourceRect = CGRectMake(anchorRect.x(), anchorRect.y(), anchorRect.width(), anchorRect.height());
m_presentingViewController = presentingViewController;
}
} // namespace WebCore
#endif // PLATFORM(IOS_FAMILY)