/* * Copyright (C) 2020 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 "WKNumberPadView.h" #if HAVE(PEPPER_UI_CORE) #import "PepperUICoreSPI.h" #import "WKNumberPadViewController.h" #import #import #import #import #import #import static BOOL isRegularDeviceVariant() { return [UIDevice currentDevice].puic_deviceVariant == PUICDeviceVariantRegular; } static CGFloat numberPadButtonSpacing() { return isRegularDeviceVariant() ? 2.5 : 2; } static CGFloat numberPadBackgroundInsetAmount() { return isRegularDeviceVariant() ? 1 : 1.25; } static CGFloat numberPadLabelFontSize() { return [UIDevice currentDevice].puic_deviceVariant == PUICDeviceVariantCompact ? 24 : 28; } const CGFloat numberPadHighlightedWhiteColorValue = 0.38; const CGFloat numberPadButtonBackgroundViewCornerRadius = 6; const CGFloat numberPadButtonHighlightScaleFactor = 1.35; const CGFloat numberPadHighlightAnimationDuration = 0.25; const CGFloat numberPadNonHighlightedButtonAlpha = 0.5; const CGFloat numberPadNonVisibleButtonScaleFactor = 0.001; const CGFloat numberPadHighlightedButtonZPosition = 100; typedef struct ColumnAndRow { NSUInteger column; NSUInteger row; } ColumnAndRow; static ColumnAndRow columnAndRowForPosition(WKNumberPadButtonPosition position) { if (!position) return { 1, 3 }; if (position == WKNumberPadButtonPositionBottomLeft) return { 0, 3 }; if (position == WKNumberPadButtonPositionBottomRight) return { 2, 3 }; return { (static_cast(position) - 1) % 3, (static_cast(position) - 1) / 3 }; } static WKNumberPadButtonPosition positionForColumnAndRow(ColumnAndRow columnAndRow) { NSUInteger column = columnAndRow.column; NSUInteger row = columnAndRow.row; if (row == 3) { if (!column) return WKNumberPadButtonPositionBottomLeft; if (column == 1) return WKNumberPadButtonPosition0; return WKNumberPadButtonPositionBottomRight; } return static_cast(1 + (3 * row + column)); } static BOOL shouldDrawBackgroundForKey(WKNumberPadKey key, WKNumberPadButtonMode mode) { switch (key) { case WKNumberPadKeyAccept: case WKNumberPadKeyToggleMode: case WKNumberPadKeyNone: return NO; case WKNumberPadKeyPlus: return mode == WKNumberPadButtonModeAlternate; default: return YES; } } static UIColor *buttonTitleColorForKey(WKNumberPadKey key) { return key == WKNumberPadKeyAccept ? [UIColor systemBlueColor] : [UIColor whiteColor]; } static NSString *buttonTitleForKey(WKNumberPadKey key) { switch (key) { case WKNumberPadKeyDash: return @"-"; case WKNumberPadKeyAsterisk: return @"*"; case WKNumberPadKeyOctothorpe: return @"#"; case WKNumberPadKeyClosingParenthesis: return @")"; case WKNumberPadKeyOpeningParenthesis: return @"("; case WKNumberPadKeyPlus: return @"+"; case WKNumberPadKeyToggleMode: return @"…"; case WKNumberPadKeyAccept: return WebCore::numberPadOKButtonTitle(); case WKNumberPadKeyNone: return nil; default: return String::number(key); } } @interface WKNumberPadButton : UIButton - (void)setVisible:(BOOL)visible duration:(NSTimeInterval)duration delay:(NSTimeInterval)startDelay; - (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated; @property (nonatomic) CGFloat backgroundInsetAmount; @property (nonatomic, readonly, getter=isVisible) BOOL visible; @property (nonatomic) CGPoint subAnchor; @property (nonatomic) WKNumberPadButtonMode buttonMode; @property (nonatomic) WKNumberPadButtonPosition buttonPosition; @property (nonatomic, readonly) WKNumberPadKey currentKey; @property (nonatomic) WKNumberPadKey defaultKey; @property (nonatomic) WKNumberPadKey alternateKey; @end @implementation WKNumberPadButton { BOOL _visible; RetainPtr _backgroundView; RetainPtr _defaultTitle; RetainPtr _alternateTitle; RetainPtr _titleFont; } - (instancetype)initWithFrame:(CGRect)frame { if (!(self = [super initWithFrame:frame])) return nil; _buttonMode = WKNumberPadButtonModeDefault; _backgroundView = adoptNS([[UIView alloc] init]); [_backgroundView setBackgroundColor:[UIColor colorWithWhite:numberPadHighlightedWhiteColorValue alpha:numberPadNonHighlightedButtonAlpha]]; [_backgroundView setUserInteractionEnabled:NO]; [self insertSubview:_backgroundView.get() atIndex:0]; _visible = YES; _backgroundInsetAmount = numberPadBackgroundInsetAmount(); self.titleLabel.adjustsFontSizeToFitWidth = YES; return self; } - (BOOL)isVisible { return _visible; } - (void)setVisible:(BOOL)visible duration:(NSTimeInterval)duration delay:(NSTimeInterval)startDelay { [self setEnabled:visible]; if (visible == _visible) return; auto applyBlock = [visible, protectedSelf = retainPtr(self)] { [protectedSelf setTransform:visible ? CGAffineTransformIdentity : CGAffineTransformMakeScale(numberPadNonVisibleButtonScaleFactor, numberPadNonVisibleButtonScaleFactor)]; [protectedSelf titleLabel].alpha = visible ? 1 : 0; }; if (duration > 0) [UIView animateWithDuration:duration delay:startDelay options:UIViewAnimationOptionAllowUserInteraction animations:applyBlock completion:nil]; else applyBlock(); _visible = visible; } - (void)setDefaultKey:(WKNumberPadKey)defaultKey { if (_defaultKey == defaultKey) return; _defaultKey = defaultKey; if (self.buttonMode == WKNumberPadButtonModeDefault) [self _updateBackgroundView]; [self setNeedsLayout]; } - (void)setAlternateKey:(WKNumberPadKey)alternateKey { if (_alternateKey == alternateKey) return; _alternateKey = alternateKey; if (self.buttonMode == WKNumberPadButtonModeAlternate) [self _updateBackgroundView]; [self setNeedsLayout]; } - (NSString *)defaultTitle { return buttonTitleForKey(self.defaultKey); } - (NSString *)alternateTitle { return buttonTitleForKey(self.alternateKey); } - (WKNumberPadKey)currentKey { return _buttonMode == WKNumberPadButtonModeAlternate ? self.alternateKey : self.defaultKey; } - (void)_updateBackgroundView { [_backgroundView setBackgroundColor:shouldDrawBackgroundForKey(self.currentKey, self.buttonMode) ? [UIColor colorWithWhite:numberPadHighlightedWhiteColorValue alpha:1] : [UIColor clearColor]]; [_backgroundView setAlpha:self.isHighlighted ? 1 : numberPadNonHighlightedButtonAlpha]; } - (CGPoint)subAnchor { return [_backgroundView layer].anchorPoint; } - (void)setSubAnchor:(CGPoint)subAnchor { [_backgroundView layer].anchorPoint = subAnchor; } - (void)setHighlighted:(BOOL)highlighted { [self setHighlighted:highlighted animated:NO]; } - (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated { self.layer.zPosition = highlighted ? numberPadHighlightedButtonZPosition : 0; [super setHighlighted:highlighted]; [self setNeedsLayout]; if (!animated) { [self _updateBackgroundView]; return; } [UIView animateWithDuration:numberPadHighlightAnimationDuration delay:0 options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState animations:[protectedSelf = retainPtr(self)] { [protectedSelf layoutIfNeeded]; [protectedSelf _updateBackgroundView]; } completion:nil]; } - (void)setEnabled:(BOOL)enabled { [super setEnabled:enabled]; self.alpha = enabled ? 1 : numberPadNonHighlightedButtonAlpha; } - (void)setButtonMode:(WKNumberPadButtonMode)buttonMode { if (_buttonMode == buttonMode) return; _buttonMode = buttonMode; [self _updateBackgroundView]; [self setNeedsLayout]; } - (void)layoutSubviews { CGRect bounds = self.bounds; UIView *titleView = self.titleLabel; titleView.transform = CGAffineTransformIdentity; UIView *imageView = self.imageView; imageView.transform = CGAffineTransformIdentity; if (_buttonMode == WKNumberPadButtonModeAlternate) { BOOL enabled = self.alternateKey != WKNumberPadKeyNone; [self setEnabled:enabled]; // This matches behavior on iOS, wherein switching to symbol keys disables any keys that don't support alternatives, but leaves their titles unchanged. [self setTitle:buttonTitleForKey(enabled ? self.alternateKey : self.defaultKey) forState:UIControlStateNormal]; [self setTitleColor:buttonTitleColorForKey(enabled ? self.alternateKey : self.defaultKey) forState:UIControlStateNormal]; } else { [self setEnabled:self.defaultKey != WKNumberPadKeyNone]; [self setTitle:self.defaultTitle forState:UIControlStateNormal]; [self setTitleColor:buttonTitleColorForKey(self.defaultKey) forState:UIControlStateNormal]; } [super layoutSubviews]; [_backgroundView setTransform:CGAffineTransformIdentity]; [_backgroundView setFrame:CGRectInset(bounds, _backgroundInsetAmount, _backgroundInsetAmount)]; [_backgroundView layer].cornerRadius = numberPadButtonBackgroundViewCornerRadius; CGAffineTransform transform = self.isHighlighted && shouldDrawBackgroundForKey(self.currentKey, self.buttonMode) ? CGAffineTransformMakeScale(numberPadButtonHighlightScaleFactor, numberPadButtonHighlightScaleFactor) : CGAffineTransformIdentity; [_backgroundView setTransform:transform]; titleView.transform = transform; imageView.transform = transform; CGRect backgroundBounds = [_backgroundView bounds]; CGPoint contentCenter = [self convertPoint:CGPointMake(backgroundBounds.size.width / 2, backgroundBounds.size.height / 2) fromView:_backgroundView.get()]; titleView.center = contentCenter; imageView.center = contentCenter; } @end @implementation WKNumberPadView { RetainPtr _trackedTouch; RetainPtr _highlightedButton; RetainPtr _keypad; RetainPtr _contentView; WeakObjCPtr _controller; } - (instancetype)initWithFrame:(CGRect)frame controller:(WKNumberPadViewController *)controller { if (!(self = [super initWithFrame:frame])) return nil; _controller = controller; _contentView = adoptNS([[UIView alloc] initWithFrame:self.bounds]); [_contentView setClipsToBounds:YES]; [_contentView setBackgroundColor:[UIColor blackColor]]; [self addSubview:_contentView.get()]; [self _initKeypad]; return self; } static WKNumberPadKey defaultKeyAtPosition(WKNumberPadButtonPosition position, WKNumberPadInputMode inputMode) { switch (position) { case WKNumberPadButtonPositionBottomLeft: if (inputMode == WKNumberPadInputModeTelephone) return WKNumberPadKeyPlus; if (inputMode == WKNumberPadInputModeNumbersOnly) return WKNumberPadKeyNone; return WKNumberPadKeyToggleMode; case WKNumberPadButtonPositionBottomRight: return WKNumberPadKeyAccept; case WKNumberPadButtonPosition0: return WKNumberPadKey0; case WKNumberPadButtonPosition1: return WKNumberPadKey1; case WKNumberPadButtonPosition2: return WKNumberPadKey2; case WKNumberPadButtonPosition3: return WKNumberPadKey3; case WKNumberPadButtonPosition4: return WKNumberPadKey4; case WKNumberPadButtonPosition5: return WKNumberPadKey5; case WKNumberPadButtonPosition6: return WKNumberPadKey6; case WKNumberPadButtonPosition7: return WKNumberPadKey7; case WKNumberPadButtonPosition8: return WKNumberPadKey8; case WKNumberPadButtonPosition9: return WKNumberPadKey9; default: ASSERT_NOT_REACHED(); return WKNumberPadKeyNone; } } static WKNumberPadKey alternateKeyAtPosition(WKNumberPadButtonPosition position) { switch (position) { case WKNumberPadButtonPositionBottomLeft: return WKNumberPadKeyToggleMode; case WKNumberPadButtonPositionBottomRight: return WKNumberPadKeyAccept; case WKNumberPadButtonPosition0: return WKNumberPadKeyDash; case WKNumberPadButtonPosition4: return WKNumberPadKeyAsterisk; case WKNumberPadButtonPosition6: return WKNumberPadKeyPlus; case WKNumberPadButtonPosition7: return WKNumberPadKeyOpeningParenthesis; case WKNumberPadButtonPosition8: return WKNumberPadKeyOctothorpe; case WKNumberPadButtonPosition9: return WKNumberPadKeyClosingParenthesis; default: return WKNumberPadKeyNone; } } - (WKNumberPadButton *)_buttonForPosition:(WKNumberPadButtonPosition)position { auto button = adoptNS([[WKNumberPadButton alloc] init]); [button setDefaultKey:defaultKeyAtPosition(position, [_controller inputMode])]; [button setAlternateKey:alternateKeyAtPosition(position)]; [button setButtonPosition:position]; [button titleLabel].font = [UIFont systemFontOfSize:numberPadLabelFontSize() weight:UIFontWeightSemibold design:(NSString *)kCTFontUIFontDesignRounded]; [button setUserInteractionEnabled:NO]; return button.autorelease(); } - (void)_initKeypad { UIView *view = _contentView.get(); _keypad = adoptNS([[UIView alloc] init]); [_keypad setUserInteractionEnabled:YES]; CGFloat xAnchors[] = { 0.0, 0.5, 1.0 }; CGFloat yAnchors[] = { 0.0, 0.5, 0.5, 1.0 }; for (unsigned x = 0; x < 3; ++x) { for (unsigned y = 0; y < 4; ++y) { WKNumberPadButton *button = [self _buttonForPosition:positionForColumnAndRow({ x, y })]; button.subAnchor = CGPointMake(xAnchors[x], yAnchors[y]); [_keypad addSubview:button]; } } [view addSubview:_keypad.get()]; } - (void)layoutSubviews { [super layoutSubviews]; [_contentView setFrame:self.bounds]; CGFloat buttonSpacing = numberPadButtonSpacing(); CGSize buttonSize = CGSizeMake((CGRectGetWidth(self.bounds) - 2 * buttonSpacing) / 3, (CGRectGetHeight(self.bounds) - 3 * buttonSpacing) / 4); [_keypad setFrame:self.bounds]; [[_keypad subviews] enumerateObjectsUsingBlock:^(WKNumberPadButton *button, NSUInteger, BOOL *) { if (!button.visible) [button setVisible:YES duration:0 delay:0]; auto columnAndRow = columnAndRowForPosition(button.buttonPosition); CGFloat insetAmount = buttonSpacing / 2; CGRect baseFrame = CGRectMake(columnAndRow.column * (buttonSize.width + buttonSpacing), columnAndRow.row * (buttonSize.height + buttonSpacing), buttonSize.width, buttonSize.height); [button setFrame:CGRectInset(baseFrame, -insetAmount, -insetAmount)]; [button setBackgroundInsetAmount:insetAmount]; }]; } - (WKNumberPadButton *)_numberPadButtonForTouch:(UITouch *)touch { CGPoint pointInViewCoordinates = [touch locationInView:self]; for (WKNumberPadButton *button in [_keypad subviews]) { CGPoint pointInButtonCoordinates = [button convertPoint:pointInViewCoordinates fromView:self]; if ([button pointInside:pointInButtonCoordinates withEvent:nil]) return button.enabled ? button : nil; } return nil; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { if (_trackedTouch) return; _trackedTouch = touches.anyObject; _highlightedButton = [self _numberPadButtonForTouch:_trackedTouch.get()]; [_highlightedButton setHighlighted:YES]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { for (UITouch *touch in touches) { if (touch != _trackedTouch) continue; WKNumberPadButton *newHighlightedButton = [self _numberPadButtonForTouch:_trackedTouch.get()]; if (newHighlightedButton == _highlightedButton) return; [_highlightedButton setHighlighted:NO]; _highlightedButton = newHighlightedButton; [_highlightedButton setHighlighted:YES]; return; } } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { for (UITouch *touch in touches) { if (touch != _trackedTouch) continue; _trackedTouch = nil; if (!_highlightedButton) return; auto selectedKey = [_highlightedButton currentKey]; [_controller didSelectKey:selectedKey]; [_highlightedButton setHighlighted:NO animated:YES]; if (selectedKey == WKNumberPadKeyToggleMode || [_highlightedButton buttonMode] == WKNumberPadButtonModeAlternate) [self toggleAlternateKeys]; _highlightedButton = nil; return; } } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { for (UITouch *touch in touches) { if (touch != _trackedTouch) continue; _trackedTouch = nil; [_highlightedButton setHighlighted:NO animated:YES]; _highlightedButton = nil; return; } } - (void)toggleAlternateKeys { for (WKNumberPadButton *button in [_keypad subviews]) { if (button.buttonMode == WKNumberPadButtonModeAlternate) button.buttonMode = WKNumberPadButtonModeDefault; else button.buttonMode = WKNumberPadButtonModeAlternate; } } @end #endif // HAVE(PEPPER_UI_CORE)