| /* |
| * 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) |