WKNumberPadView.mm [plain text]
/*
* 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 PLATFORM(WATCHOS)
#import "PepperUICoreSPI.h"
#import "WKNumberPadViewController.h"
#import <WebCore/LocalizedStrings.h>
#import <pal/spi/cocoa/CoreTextSPI.h>
#import <wtf/NeverDestroyed.h>
#import <wtf/RetainPtr.h>
#import <wtf/WeakObjCPtr.h>
#import <wtf/text/WTFString.h>
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<NSUInteger>(position) - 1) % 3, (static_cast<NSUInteger>(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<WKNumberPadButtonPosition>(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<UIView> _backgroundView;
RetainPtr<NSString> _defaultTitle;
RetainPtr<NSString> _alternateTitle;
RetainPtr<UIFont> _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<UITouch> _trackedTouch;
RetainPtr<WKNumberPadButton> _highlightedButton;
RetainPtr<UIView> _keypad;
RetainPtr<UIView> _contentView;
WeakObjCPtr<WKNumberPadViewController> _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
{
WKNumberPadButton *button = [[[WKNumberPadButton alloc] init] autorelease];
button.defaultKey = defaultKeyAtPosition(position, [_controller inputMode]);
button.alternateKey = alternateKeyAtPosition(position);
button.buttonPosition = position;
button.titleLabel.font = [UIFont systemFontOfSize:numberPadLabelFontSize() weight:UIFontWeightSemibold design:(NSString *)kCTFontUIFontDesignRounded];
button.userInteractionEnabled = NO;
return button;
}
- (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<UITouch *> *)touches withEvent:(UIEvent *)event
{
if (_trackedTouch)
return;
_trackedTouch = touches.anyObject;
_highlightedButton = [self _numberPadButtonForTouch:_trackedTouch.get()];
[_highlightedButton setHighlighted:YES];
}
- (void)touchesMoved:(NSSet<UITouch *> *)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<UITouch *> *)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<UITouch *> *)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 // PLATFORM(WATCHOS)