重构键盘长按删除
This commit is contained in:
22
CustomKeyboard/Utils/KBBackspaceLongPressHandler.h
Normal file
22
CustomKeyboard/Utils/KBBackspaceLongPressHandler.h
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// KBBackspaceLongPressHandler.h
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface KBBackspaceLongPressHandler : NSObject
|
||||||
|
|
||||||
|
- (instancetype)initWithContainerView:(UIView *)containerView;
|
||||||
|
|
||||||
|
/// 配置删除按钮(包含长按删除与“立刻清空”提示)
|
||||||
|
- (void)bindDeleteButton:(nullable UIView *)button;
|
||||||
|
|
||||||
|
/// 触发“立刻清空”逻辑(可用于功能面板的清空按钮)
|
||||||
|
- (void)performClearAction;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
475
CustomKeyboard/Utils/KBBackspaceLongPressHandler.m
Normal file
475
CustomKeyboard/Utils/KBBackspaceLongPressHandler.m
Normal file
@@ -0,0 +1,475 @@
|
|||||||
|
//
|
||||||
|
// KBBackspaceLongPressHandler.m
|
||||||
|
// CustomKeyboard
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "KBBackspaceLongPressHandler.h"
|
||||||
|
#import "KBResponderUtils.h"
|
||||||
|
#import "KBSkinManager.h"
|
||||||
|
|
||||||
|
static const NSTimeInterval kKBBackspaceLongPressMinDuration = 0.35;
|
||||||
|
static const NSTimeInterval kKBBackspaceRepeatInterval = 0.06;
|
||||||
|
static const NSTimeInterval kKBBackspaceChunkStartDelay = 1.0;
|
||||||
|
static const NSTimeInterval kKBBackspaceChunkRepeatInterval = 0.1;
|
||||||
|
static const NSTimeInterval kKBBackspaceChunkFastDelay = 1.4;
|
||||||
|
static const NSInteger kKBBackspaceChunkSize = 6;
|
||||||
|
static const NSInteger kKBBackspaceChunkSizeFast = 12;
|
||||||
|
static const CGFloat kKBBackspaceClearLabelCornerRadius = 8.0;
|
||||||
|
static const CGFloat kKBBackspaceClearLabelHeight = 26.0;
|
||||||
|
static const CGFloat kKBBackspaceClearLabelPaddingX = 10.0;
|
||||||
|
static const CGFloat kKBBackspaceClearLabelTopGap = 6.0;
|
||||||
|
static const CGFloat kKBBackspaceClearLabelHorizontalInset = 6.0;
|
||||||
|
static const NSInteger kKBBackspaceClearBatchSize = 24;
|
||||||
|
static const NSTimeInterval kKBBackspaceClearBatchInterval = 0.005;
|
||||||
|
static const NSInteger kKBBackspaceClearMaxDeletes = 10000;
|
||||||
|
static const NSInteger kKBBackspaceClearEmptyContextMaxRounds = 40;
|
||||||
|
static const NSInteger kKBBackspaceClearMaxStep = 80;
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||||
|
KBBackspaceChunkClassUnknown = 0,
|
||||||
|
KBBackspaceChunkClassWhitespace,
|
||||||
|
KBBackspaceChunkClassASCIIWord,
|
||||||
|
KBBackspaceChunkClassPunctuation,
|
||||||
|
KBBackspaceChunkClassOther
|
||||||
|
};
|
||||||
|
|
||||||
|
@interface KBBackspaceLongPressHandler ()
|
||||||
|
@property (nonatomic, weak) UIView *containerView;
|
||||||
|
@property (nonatomic, weak) UIView *backspaceButton;
|
||||||
|
@property (nonatomic, strong) UILongPressGestureRecognizer *longPress;
|
||||||
|
@property (nonatomic, assign) BOOL backspaceHoldActive;
|
||||||
|
@property (nonatomic, assign) NSTimeInterval backspaceHoldStartTime;
|
||||||
|
@property (nonatomic, assign) BOOL backspaceChunkModeActive;
|
||||||
|
@property (nonatomic, assign) BOOL backspaceClearHighlighted;
|
||||||
|
@property (nonatomic, assign) NSUInteger backspaceHoldToken;
|
||||||
|
@property (nonatomic, assign) BOOL backspaceHasLastTouchPoint;
|
||||||
|
@property (nonatomic, assign) CGPoint backspaceLastTouchPointInSelf;
|
||||||
|
@property (nonatomic, assign) NSUInteger backspaceClearToken;
|
||||||
|
@property (nonatomic, strong) UILabel *backspaceClearLabel;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation KBBackspaceLongPressHandler
|
||||||
|
|
||||||
|
- (instancetype)initWithContainerView:(UIView *)containerView {
|
||||||
|
if (self = [super init]) {
|
||||||
|
_containerView = containerView;
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)bindDeleteButton:(UIView *)button {
|
||||||
|
if (self.backspaceButton == button) { return; }
|
||||||
|
|
||||||
|
if (self.longPress && self.backspaceButton) {
|
||||||
|
[self.backspaceButton removeGestureRecognizer:self.longPress];
|
||||||
|
}
|
||||||
|
self.backspaceButton = button;
|
||||||
|
self.backspaceHoldActive = NO;
|
||||||
|
self.backspaceChunkModeActive = NO;
|
||||||
|
self.backspaceClearHighlighted = NO;
|
||||||
|
self.backspaceHasLastTouchPoint = NO;
|
||||||
|
self.backspaceHoldToken += 1;
|
||||||
|
[self kb_hideBackspaceClearLabel];
|
||||||
|
|
||||||
|
if (!button) { return; }
|
||||||
|
|
||||||
|
self.longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self
|
||||||
|
action:@selector(onBackspaceLongPress:)];
|
||||||
|
self.longPress.minimumPressDuration = kKBBackspaceLongPressMinDuration;
|
||||||
|
self.longPress.allowableMovement = CGFLOAT_MAX;
|
||||||
|
self.longPress.cancelsTouchesInView = YES;
|
||||||
|
[button addGestureRecognizer:self.longPress];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)performClearAction {
|
||||||
|
[self kb_clearAllInput];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Long Press
|
||||||
|
|
||||||
|
- (void)onBackspaceLongPress:(UILongPressGestureRecognizer *)gr {
|
||||||
|
UIView *hostView = [self kb_hostView];
|
||||||
|
if (!hostView) { return; }
|
||||||
|
if (gr) {
|
||||||
|
self.backspaceLastTouchPointInSelf = [gr locationInView:hostView];
|
||||||
|
self.backspaceHasLastTouchPoint = YES;
|
||||||
|
}
|
||||||
|
switch (gr.state) {
|
||||||
|
case UIGestureRecognizerStateBegan: {
|
||||||
|
self.backspaceHoldToken += 1;
|
||||||
|
NSUInteger token = self.backspaceHoldToken;
|
||||||
|
self.backspaceHoldActive = YES;
|
||||||
|
self.backspaceHoldStartTime = [NSDate date].timeIntervalSinceReferenceDate;
|
||||||
|
self.backspaceChunkModeActive = NO;
|
||||||
|
[self kb_setBackspaceClearHighlighted:NO];
|
||||||
|
[self kb_hideBackspaceClearLabel];
|
||||||
|
[self kb_backspaceStepForToken:token];
|
||||||
|
} break;
|
||||||
|
case UIGestureRecognizerStateChanged: {
|
||||||
|
[self kb_handleBackspaceLongPressChanged:gr];
|
||||||
|
} break;
|
||||||
|
case UIGestureRecognizerStateEnded:
|
||||||
|
case UIGestureRecognizerStateCancelled:
|
||||||
|
case UIGestureRecognizerStateFailed: {
|
||||||
|
[self kb_handleBackspaceLongPressEnded:gr];
|
||||||
|
} break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Delete Steps
|
||||||
|
|
||||||
|
- (void)kb_backspaceStepForToken:(NSUInteger)token {
|
||||||
|
if (!self.backspaceHoldActive) { return; }
|
||||||
|
if (token != self.backspaceHoldToken) { return; }
|
||||||
|
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||||
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||||
|
if (!ivc) { self.backspaceHoldActive = NO; return; }
|
||||||
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
|
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
||||||
|
NSTimeInterval elapsed = [NSDate date].timeIntervalSinceReferenceDate - self.backspaceHoldStartTime;
|
||||||
|
NSInteger deleteCount = 1;
|
||||||
|
if (before.length > 0) {
|
||||||
|
deleteCount = [self kb_backspaceDeleteCountForContext:before elapsed:elapsed];
|
||||||
|
}
|
||||||
|
if (!self.backspaceChunkModeActive && elapsed >= kKBBackspaceChunkStartDelay) {
|
||||||
|
self.backspaceChunkModeActive = YES;
|
||||||
|
[self kb_showBackspaceClearLabelIfNeeded];
|
||||||
|
}
|
||||||
|
for (NSInteger i = 0; i < deleteCount; i++) {
|
||||||
|
[proxy deleteBackward];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSTimeInterval interval = [self kb_backspaceRepeatIntervalForElapsed:elapsed];
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
|
||||||
|
(int64_t)(interval * NSEC_PER_SEC)),
|
||||||
|
dispatch_get_main_queue(), ^{
|
||||||
|
__strong typeof(weakSelf) selfStrong = weakSelf;
|
||||||
|
[selfStrong kb_backspaceStepForToken:token];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSTimeInterval)kb_backspaceRepeatIntervalForElapsed:(NSTimeInterval)elapsed {
|
||||||
|
if (elapsed >= kKBBackspaceChunkStartDelay) {
|
||||||
|
return kKBBackspaceChunkRepeatInterval;
|
||||||
|
}
|
||||||
|
return kKBBackspaceRepeatInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSInteger)kb_backspaceDeleteCountForContext:(NSString *)context elapsed:(NSTimeInterval)elapsed {
|
||||||
|
if (elapsed < kKBBackspaceChunkStartDelay) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
NSInteger maxCount = (elapsed >= kKBBackspaceChunkFastDelay)
|
||||||
|
? kKBBackspaceChunkSizeFast : kKBBackspaceChunkSize;
|
||||||
|
return [self kb_backspaceChunkDeleteCountForContext:context maxCount:maxCount];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSInteger)kb_backspaceChunkDeleteCountForContext:(NSString *)context maxCount:(NSInteger)maxCount {
|
||||||
|
if (context.length == 0) { return 1; }
|
||||||
|
|
||||||
|
static NSCharacterSet *whitespaceSet = nil;
|
||||||
|
static NSCharacterSet *asciiWordSet = nil;
|
||||||
|
static NSCharacterSet *punctuationSet = nil;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||||||
|
asciiWordSet = [NSCharacterSet characterSetWithCharactersInString:
|
||||||
|
@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"];
|
||||||
|
punctuationSet = [NSCharacterSet punctuationCharacterSet];
|
||||||
|
});
|
||||||
|
|
||||||
|
__block NSInteger deleteCount = 0;
|
||||||
|
__block KBBackspaceChunkClass chunkClass = KBBackspaceChunkClassUnknown;
|
||||||
|
[context enumerateSubstringsInRange:NSMakeRange(0, context.length)
|
||||||
|
options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse
|
||||||
|
usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) {
|
||||||
|
if (substring.length == 0) { return; }
|
||||||
|
KBBackspaceChunkClass currentClass = KBBackspaceChunkClassOther;
|
||||||
|
if ([substring rangeOfCharacterFromSet:whitespaceSet].location != NSNotFound) {
|
||||||
|
currentClass = KBBackspaceChunkClassWhitespace;
|
||||||
|
} else if ([substring rangeOfCharacterFromSet:asciiWordSet].location != NSNotFound) {
|
||||||
|
currentClass = KBBackspaceChunkClassASCIIWord;
|
||||||
|
} else if ([substring rangeOfCharacterFromSet:punctuationSet].location != NSNotFound) {
|
||||||
|
currentClass = KBBackspaceChunkClassPunctuation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunkClass == KBBackspaceChunkClassUnknown) {
|
||||||
|
chunkClass = currentClass;
|
||||||
|
} else if (chunkClass != currentClass) {
|
||||||
|
*stop = YES;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteCount += 1;
|
||||||
|
if (deleteCount >= maxCount) {
|
||||||
|
*stop = YES;
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
|
||||||
|
return MAX(deleteCount, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSInteger)kb_clearDeleteCountForContext:(NSString *)context
|
||||||
|
hitBoundary:(BOOL *)hitBoundary {
|
||||||
|
if (context.length == 0) { return kKBBackspaceClearBatchSize; }
|
||||||
|
|
||||||
|
static NSCharacterSet *sentenceBoundarySet = nil;
|
||||||
|
static NSCharacterSet *whitespaceSet = nil;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;。!?;…\n"];
|
||||||
|
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||||||
|
});
|
||||||
|
|
||||||
|
NSInteger length = context.length;
|
||||||
|
NSInteger end = length;
|
||||||
|
while (end > 0) {
|
||||||
|
unichar ch = [context characterAtIndex:end - 1];
|
||||||
|
if ([whitespaceSet characterIsMember:ch]) {
|
||||||
|
end -= 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NSInteger searchEnd = end;
|
||||||
|
while (searchEnd > 0) {
|
||||||
|
unichar ch = [context characterAtIndex:searchEnd - 1];
|
||||||
|
if ([sentenceBoundarySet characterIsMember:ch]) {
|
||||||
|
searchEnd -= 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSInteger boundaryIndex = NSNotFound;
|
||||||
|
for (NSInteger i = searchEnd - 1; i >= 0; i--) {
|
||||||
|
unichar ch = [context characterAtIndex:i];
|
||||||
|
if ([sentenceBoundarySet characterIsMember:ch]) {
|
||||||
|
boundaryIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL boundaryFound = (boundaryIndex != NSNotFound);
|
||||||
|
NSInteger deleteCount = length;
|
||||||
|
if (boundaryIndex != NSNotFound) {
|
||||||
|
deleteCount = length - (boundaryIndex + 1);
|
||||||
|
}
|
||||||
|
deleteCount = MAX(deleteCount, 1);
|
||||||
|
if (hitBoundary) {
|
||||||
|
*hitBoundary = boundaryFound;
|
||||||
|
}
|
||||||
|
return MIN(deleteCount, kKBBackspaceClearMaxStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Long Press State
|
||||||
|
|
||||||
|
- (void)kb_handleBackspaceLongPressChanged:(UILongPressGestureRecognizer *)gr {
|
||||||
|
if (!self.backspaceHoldActive) { return; }
|
||||||
|
NSTimeInterval elapsed = [NSDate date].timeIntervalSinceReferenceDate - self.backspaceHoldStartTime;
|
||||||
|
if (elapsed < kKBBackspaceChunkStartDelay) { return; }
|
||||||
|
[self kb_showBackspaceClearLabelIfNeeded];
|
||||||
|
UIView *hostView = [self kb_hostView];
|
||||||
|
if (!hostView) { return; }
|
||||||
|
CGPoint point = [gr locationInView:hostView];
|
||||||
|
self.backspaceLastTouchPointInSelf = point;
|
||||||
|
self.backspaceHasLastTouchPoint = YES;
|
||||||
|
BOOL inside = [self kb_isPointInsideBackspaceClearLabel:point];
|
||||||
|
[self kb_setBackspaceClearHighlighted:inside];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_handleBackspaceLongPressEnded:(UILongPressGestureRecognizer *)gr {
|
||||||
|
BOOL shouldClear = self.backspaceClearHighlighted;
|
||||||
|
if (!shouldClear) {
|
||||||
|
UIView *hostView = [self kb_hostView];
|
||||||
|
CGPoint point = CGPointZero;
|
||||||
|
if (gr && hostView) {
|
||||||
|
point = [gr locationInView:hostView];
|
||||||
|
} else if (self.backspaceHasLastTouchPoint) {
|
||||||
|
point = self.backspaceLastTouchPointInSelf;
|
||||||
|
}
|
||||||
|
shouldClear = [self kb_isPointInsideBackspaceClearLabel:point];
|
||||||
|
}
|
||||||
|
self.backspaceHoldActive = NO;
|
||||||
|
self.backspaceChunkModeActive = NO;
|
||||||
|
self.backspaceHoldToken += 1;
|
||||||
|
self.backspaceHasLastTouchPoint = NO;
|
||||||
|
[self kb_hideBackspaceClearLabel];
|
||||||
|
if (shouldClear) {
|
||||||
|
[self kb_clearAllInput];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Clear Label
|
||||||
|
|
||||||
|
- (void)kb_showBackspaceClearLabelIfNeeded {
|
||||||
|
UIView *hostView = [self kb_hostView];
|
||||||
|
if (!hostView || !self.backspaceButton) { return; }
|
||||||
|
UILabel *label = self.backspaceClearLabel;
|
||||||
|
[self kb_refreshBackspaceClearLabelColors];
|
||||||
|
if (!label.superview) {
|
||||||
|
[hostView addSubview:label];
|
||||||
|
}
|
||||||
|
[self kb_updateBackspaceClearLabelFrame];
|
||||||
|
[hostView bringSubviewToFront:label];
|
||||||
|
if (label.hidden) {
|
||||||
|
label.alpha = 0.0;
|
||||||
|
label.hidden = NO;
|
||||||
|
[self kb_playLightHaptic];
|
||||||
|
[UIView animateWithDuration:0.12 animations:^{
|
||||||
|
label.alpha = 1.0;
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_hideBackspaceClearLabel {
|
||||||
|
if (!_backspaceClearLabel || _backspaceClearLabel.hidden) { return; }
|
||||||
|
_backspaceClearLabel.hidden = YES;
|
||||||
|
_backspaceClearLabel.alpha = 1.0;
|
||||||
|
[self kb_setBackspaceClearHighlighted:NO];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_updateBackspaceClearLabelFrame {
|
||||||
|
UIView *hostView = [self kb_hostView];
|
||||||
|
if (!hostView || !self.backspaceButton || !self.backspaceClearLabel) { return; }
|
||||||
|
CGRect btnFrame = [self.backspaceButton convertRect:self.backspaceButton.bounds toView:hostView];
|
||||||
|
UILabel *label = self.backspaceClearLabel;
|
||||||
|
CGSize textSize = [label sizeThatFits:CGSizeMake(CGFLOAT_MAX, kKBBackspaceClearLabelHeight)];
|
||||||
|
CGFloat width = MAX(textSize.width + kKBBackspaceClearLabelPaddingX * 2.0, 60.0);
|
||||||
|
CGFloat height = kKBBackspaceClearLabelHeight;
|
||||||
|
CGFloat x = CGRectGetMidX(btnFrame) - width * 0.5;
|
||||||
|
CGFloat y = CGRectGetMinY(btnFrame) - height - kKBBackspaceClearLabelTopGap;
|
||||||
|
if (x < kKBBackspaceClearLabelHorizontalInset) { x = kKBBackspaceClearLabelHorizontalInset; }
|
||||||
|
CGFloat maxX = CGRectGetWidth(hostView.bounds) - kKBBackspaceClearLabelHorizontalInset - width;
|
||||||
|
if (x > maxX) { x = maxX; }
|
||||||
|
if (y < 0) { y = 0; }
|
||||||
|
label.frame = CGRectIntegral(CGRectMake(x, y, width, height));
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)kb_isPointInsideBackspaceClearLabel:(CGPoint)point {
|
||||||
|
if (!self.backspaceClearLabel || self.backspaceClearLabel.hidden) { return NO; }
|
||||||
|
[self kb_updateBackspaceClearLabelFrame];
|
||||||
|
CGRect hitFrame = CGRectInset(self.backspaceClearLabel.frame, -12.0, -10.0);
|
||||||
|
return CGRectContainsPoint(hitFrame, point);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_setBackspaceClearHighlighted:(BOOL)highlighted {
|
||||||
|
if (self.backspaceClearHighlighted == highlighted) { return; }
|
||||||
|
self.backspaceClearHighlighted = highlighted;
|
||||||
|
[self kb_refreshBackspaceClearLabelColors];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_refreshBackspaceClearLabelColors {
|
||||||
|
UILabel *label = self.backspaceClearLabel;
|
||||||
|
label.textColor = [KBSkinManager shared].current.keyTextColor ?: UIColor.blackColor;
|
||||||
|
label.backgroundColor = self.backspaceClearHighlighted
|
||||||
|
? [self kb_backspaceClearLabelHighlightedColor]
|
||||||
|
: [self kb_backspaceClearLabelNormalColor];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIColor *)kb_backspaceClearLabelNormalColor {
|
||||||
|
KBSkinTheme *t = [KBSkinManager shared].current;
|
||||||
|
return t.keyHighlightBackground ?: [UIColor colorWithWhite:0.9 alpha:1.0];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIColor *)kb_backspaceClearLabelHighlightedColor {
|
||||||
|
KBSkinTheme *t = [KBSkinManager shared].current;
|
||||||
|
return t.accentColor ?: t.keyHighlightBackground ?: [UIColor colorWithWhite:0.8 alpha:1.0];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_playLightHaptic {
|
||||||
|
if (@available(iOS 10.0, *)) {
|
||||||
|
UIImpactFeedbackGenerator *gen = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
|
||||||
|
[gen prepare];
|
||||||
|
[gen impactOccurred];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UILabel *)backspaceClearLabel {
|
||||||
|
if (!_backspaceClearLabel) {
|
||||||
|
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
|
||||||
|
label.text = @"立刻清空";
|
||||||
|
label.textAlignment = NSTextAlignmentCenter;
|
||||||
|
label.font = [UIFont systemFontOfSize:12 weight:UIFontWeightSemibold];
|
||||||
|
label.textColor = [KBSkinManager shared].current.keyTextColor ?: UIColor.blackColor;
|
||||||
|
label.backgroundColor = [self kb_backspaceClearLabelNormalColor];
|
||||||
|
label.layer.cornerRadius = kKBBackspaceClearLabelCornerRadius;
|
||||||
|
label.layer.masksToBounds = YES;
|
||||||
|
label.hidden = YES;
|
||||||
|
label.userInteractionEnabled = NO;
|
||||||
|
_backspaceClearLabel = label;
|
||||||
|
}
|
||||||
|
return _backspaceClearLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Clear
|
||||||
|
|
||||||
|
- (void)kb_clearAllInput {
|
||||||
|
self.backspaceClearToken += 1;
|
||||||
|
NSUInteger token = self.backspaceClearToken;
|
||||||
|
[self kb_clearAllInputStepForToken:token guard:0 emptyRounds:0];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_clearAllInputStepForToken:(NSUInteger)token
|
||||||
|
guard:(NSInteger)guard
|
||||||
|
emptyRounds:(NSInteger)emptyRounds {
|
||||||
|
if (token != self.backspaceClearToken) { return; }
|
||||||
|
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||||
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||||
|
if (!ivc) { return; }
|
||||||
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
|
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
||||||
|
NSInteger count = before.length;
|
||||||
|
NSInteger batch = 0;
|
||||||
|
NSInteger nextEmptyRounds = emptyRounds;
|
||||||
|
BOOL hitBoundary = NO;
|
||||||
|
if (count > 0) {
|
||||||
|
batch = [self kb_clearDeleteCountForContext:before hitBoundary:&hitBoundary];
|
||||||
|
nextEmptyRounds = 0;
|
||||||
|
} else {
|
||||||
|
batch = kKBBackspaceClearBatchSize;
|
||||||
|
nextEmptyRounds = emptyRounds + 1;
|
||||||
|
}
|
||||||
|
if (batch <= 0) { batch = 1; }
|
||||||
|
|
||||||
|
if (guard >= kKBBackspaceClearMaxDeletes ||
|
||||||
|
nextEmptyRounds > kKBBackspaceClearEmptyContextMaxRounds) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (NSInteger i = 0; i < batch; i++) {
|
||||||
|
[proxy deleteBackward];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSInteger nextGuard = guard + batch;
|
||||||
|
BOOL shouldContinue = NO;
|
||||||
|
if (count > 0 && !hitBoundary) {
|
||||||
|
if (count > batch) {
|
||||||
|
shouldContinue = YES;
|
||||||
|
} else if ([proxy hasText]) {
|
||||||
|
shouldContinue = YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldContinue) { return; }
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
|
||||||
|
(int64_t)(kKBBackspaceClearBatchInterval * NSEC_PER_SEC)),
|
||||||
|
dispatch_get_main_queue(), ^{
|
||||||
|
__strong typeof(weakSelf) selfStrong = weakSelf;
|
||||||
|
[selfStrong kb_clearAllInputStepForToken:token
|
||||||
|
guard:nextGuard
|
||||||
|
emptyRounds:nextEmptyRounds];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Helpers
|
||||||
|
|
||||||
|
- (UIView *)kb_hostView {
|
||||||
|
if (self.containerView) { return self.containerView; }
|
||||||
|
return self.backspaceButton.superview;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
#import "KBTagItemModel.h"
|
#import "KBTagItemModel.h"
|
||||||
#import <MJExtension/MJExtension.h>
|
#import <MJExtension/MJExtension.h>
|
||||||
#import "KBBizCode.h"
|
#import "KBBizCode.h"
|
||||||
|
#import "KBBackspaceLongPressHandler.h"
|
||||||
|
|
||||||
@interface KBFunctionView () <KBFunctionBarViewDelegate, KBStreamOverlayViewDelegate, KBFunctionTagListViewDelegate>
|
@interface KBFunctionView () <KBFunctionBarViewDelegate, KBStreamOverlayViewDelegate, KBFunctionTagListViewDelegate>
|
||||||
// UI
|
// UI
|
||||||
@@ -59,6 +60,7 @@
|
|||||||
// UL 双路兜底
|
// UL 双路兜底
|
||||||
@property (nonatomic, assign) NSUInteger kb_ulSeq; // 当前 UL 发起序号
|
@property (nonatomic, assign) NSUInteger kb_ulSeq; // 当前 UL 发起序号
|
||||||
@property (nonatomic, assign) BOOL kb_ulHandledFlag; // 主 App 已确认处理 UL
|
@property (nonatomic, assign) BOOL kb_ulHandledFlag; // 主 App 已确认处理 UL
|
||||||
|
@property (nonatomic, strong) KBBackspaceLongPressHandler *backspaceHandler;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation KBFunctionView
|
@implementation KBFunctionView
|
||||||
@@ -67,6 +69,7 @@
|
|||||||
if (self = [super initWithFrame:frame]) {
|
if (self = [super initWithFrame:frame]) {
|
||||||
// 背景使用当前主题强调色
|
// 背景使用当前主题强调色
|
||||||
[self kb_applyTheme];
|
[self kb_applyTheme];
|
||||||
|
self.backspaceHandler = [[KBBackspaceLongPressHandler alloc] initWithContainerView:self];
|
||||||
|
|
||||||
[self setupUI];
|
[self setupUI];
|
||||||
// [self reloadDemoData];
|
// [self reloadDemoData];
|
||||||
@@ -770,22 +773,9 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C
|
|||||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
[proxy deleteBackward];
|
[proxy deleteBackward];
|
||||||
}
|
}
|
||||||
- (void)onTapClear {
|
- (void)onTapClear {
|
||||||
NSLog(@"点击:清空");
|
NSLog(@"点击:清空");
|
||||||
// 连续删除:仅清空光标之前的输入(不改动 pasteView 的内容)
|
[self.backspaceHandler performClearAction];
|
||||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
|
||||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
|
||||||
// 逐批读取 documentContextBeforeInput 并删除,避免 50 字符窗口限制带来的残留
|
|
||||||
NSInteger guard = 0; // 上限保护,避免极端情况下长时间阻塞
|
|
||||||
while (guard < 10000) {
|
|
||||||
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
|
||||||
NSInteger count = before.length;
|
|
||||||
if (count <= 0) { break; } // 光标前已无内容
|
|
||||||
for (NSInteger i = 0; i < count; i++) {
|
|
||||||
[proxy deleteBackward];
|
|
||||||
}
|
|
||||||
guard += count;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
- (void)onTapSend {
|
- (void)onTapSend {
|
||||||
NSLog(@"点击:发送");
|
NSLog(@"点击:发送");
|
||||||
@@ -872,6 +862,7 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C
|
|||||||
[_deleteButtonInternal setImage:[UIImage imageNamed:@"kb_del_icon"] forState:UIControlStateNormal];
|
[_deleteButtonInternal setImage:[UIImage imageNamed:@"kb_del_icon"] forState:UIControlStateNormal];
|
||||||
|
|
||||||
[_deleteButtonInternal addTarget:self action:@selector(onTapDelete) forControlEvents:UIControlEventTouchUpInside];
|
[_deleteButtonInternal addTarget:self action:@selector(onTapDelete) forControlEvents:UIControlEventTouchUpInside];
|
||||||
|
[self.backspaceHandler bindDeleteButton:_deleteButtonInternal];
|
||||||
}
|
}
|
||||||
return _deleteButtonInternal;
|
return _deleteButtonInternal;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,32 +6,15 @@
|
|||||||
#import "KBKeyboardView.h"
|
#import "KBKeyboardView.h"
|
||||||
#import "KBKeyButton.h"
|
#import "KBKeyButton.h"
|
||||||
#import "KBKey.h"
|
#import "KBKey.h"
|
||||||
#import "KBResponderUtils.h" // 封装的响应链工具
|
|
||||||
#import "KBSkinManager.h"
|
#import "KBSkinManager.h"
|
||||||
#import "KBKeyPreviewView.h"
|
#import "KBKeyPreviewView.h"
|
||||||
|
#import "KBBackspaceLongPressHandler.h"
|
||||||
|
|
||||||
// UI 常量统一管理,方便后续调试样式(以 375 宽设计稿为基准,通过 KBFit 做等比缩放)
|
// UI 常量统一管理,方便后续调试样式(以 375 宽设计稿为基准,通过 KBFit 做等比缩放)
|
||||||
#define kKBRowVerticalSpacing KBFit(8.0f)
|
#define kKBRowVerticalSpacing KBFit(8.0f)
|
||||||
#define kKBRowHorizontalInset KBFit(6.0f)
|
#define kKBRowHorizontalInset KBFit(6.0f)
|
||||||
#define kKBRowHeight KBFit(40.0f)
|
#define kKBRowHeight KBFit(40.0f)
|
||||||
|
|
||||||
static const NSTimeInterval kKBBackspaceLongPressMinDuration = 0.35;
|
|
||||||
static const NSTimeInterval kKBBackspaceRepeatInterval = 0.06;
|
|
||||||
static const NSTimeInterval kKBBackspaceChunkStartDelay = 0.1;
|
|
||||||
static const NSTimeInterval kKBBackspaceChunkRepeatInterval = 0.1;
|
|
||||||
static const NSTimeInterval kKBBackspaceChunkFastDelay = 1.4;
|
|
||||||
static const NSInteger kKBBackspaceChunkSize = 6;
|
|
||||||
static const NSInteger kKBBackspaceChunkSizeFast = 12;
|
|
||||||
static const CGFloat kKBBackspaceClearLabelCornerRadius = 8.0;
|
|
||||||
static const CGFloat kKBBackspaceClearLabelHeight = 26.0;
|
|
||||||
static const CGFloat kKBBackspaceClearLabelPaddingX = 10.0;
|
|
||||||
static const CGFloat kKBBackspaceClearLabelTopGap = 6.0;
|
|
||||||
static const NSInteger kKBBackspaceClearBatchSize = 24;
|
|
||||||
static const NSTimeInterval kKBBackspaceClearBatchInterval = 0.005;
|
|
||||||
static const NSInteger kKBBackspaceClearMaxDeletes = 10000;
|
|
||||||
static const NSInteger kKBBackspaceClearEmptyContextMaxRounds = 40;
|
|
||||||
static const NSInteger kKBBackspaceClearMaxStep = 80;
|
|
||||||
|
|
||||||
static const NSTimeInterval kKBPreviewShowDuration = 0.08;
|
static const NSTimeInterval kKBPreviewShowDuration = 0.08;
|
||||||
static const NSTimeInterval kKBPreviewHideDuration = 0.06;
|
static const NSTimeInterval kKBPreviewHideDuration = 0.06;
|
||||||
|
|
||||||
@@ -42,31 +25,13 @@ static const CGFloat kKBSpaceWidthMultiplier = 3.0;
|
|||||||
// 第二行字母行的左右占位比例(用于居中)
|
// 第二行字母行的左右占位比例(用于居中)
|
||||||
static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5;
|
static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5;
|
||||||
|
|
||||||
typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|
||||||
KBBackspaceChunkClassUnknown = 0,
|
|
||||||
KBBackspaceChunkClassWhitespace,
|
|
||||||
KBBackspaceChunkClassASCIIWord,
|
|
||||||
KBBackspaceChunkClassPunctuation,
|
|
||||||
KBBackspaceChunkClassOther
|
|
||||||
};
|
|
||||||
|
|
||||||
@interface KBKeyboardView ()
|
@interface KBKeyboardView ()
|
||||||
@property (nonatomic, strong) UIView *row1;
|
@property (nonatomic, strong) UIView *row1;
|
||||||
@property (nonatomic, strong) UIView *row2;
|
@property (nonatomic, strong) UIView *row2;
|
||||||
@property (nonatomic, strong) UIView *row3;
|
@property (nonatomic, strong) UIView *row3;
|
||||||
@property (nonatomic, strong) UIView *row4;
|
@property (nonatomic, strong) UIView *row4;
|
||||||
@property (nonatomic, strong) NSArray<NSArray<KBKey *> *> *keysForRows;
|
@property (nonatomic, strong) NSArray<NSArray<KBKey *> *> *keysForRows;
|
||||||
// 长按退格的一次次删除控制标记(不使用 NSTimer,仅用 GCD 递归调度)
|
@property (nonatomic, strong) KBBackspaceLongPressHandler *backspaceHandler;
|
||||||
@property (nonatomic, assign) BOOL backspaceHoldActive;
|
|
||||||
@property (nonatomic, assign) NSTimeInterval backspaceHoldStartTime;
|
|
||||||
@property (nonatomic, assign) BOOL backspaceChunkModeActive;
|
|
||||||
@property (nonatomic, assign) BOOL backspaceClearHighlighted;
|
|
||||||
@property (nonatomic, assign) NSUInteger backspaceHoldToken;
|
|
||||||
@property (nonatomic, assign) BOOL backspaceHasLastTouchPoint;
|
|
||||||
@property (nonatomic, assign) CGPoint backspaceLastTouchPointInSelf;
|
|
||||||
@property (nonatomic, assign) NSUInteger backspaceClearToken;
|
|
||||||
@property (nonatomic, weak) KBKeyButton *backspaceButton;
|
|
||||||
@property (nonatomic, strong) UILabel *backspaceClearLabel;
|
|
||||||
@property (nonatomic, strong) KBKeyPreviewView *previewView;
|
@property (nonatomic, strong) KBKeyPreviewView *previewView;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@@ -79,6 +44,7 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
// 默认小写:与需求一致,初始不开启 Shift
|
// 默认小写:与需求一致,初始不开启 Shift
|
||||||
_shiftOn = NO;
|
_shiftOn = NO;
|
||||||
_symbolsMoreOn = NO; // 数字面板默认第一页(123)
|
_symbolsMoreOn = NO; // 数字面板默认第一页(123)
|
||||||
|
self.backspaceHandler = [[KBBackspaceLongPressHandler alloc] initWithContainerView:self];
|
||||||
[self buildBase];
|
[self buildBase];
|
||||||
[self reloadKeys];
|
[self reloadKeys];
|
||||||
}
|
}
|
||||||
@@ -127,10 +93,7 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
#pragma mark - Public
|
#pragma mark - Public
|
||||||
|
|
||||||
- (void)reloadKeys {
|
- (void)reloadKeys {
|
||||||
self.backspaceButton = nil;
|
[self.backspaceHandler bindDeleteButton:nil];
|
||||||
self.backspaceChunkModeActive = NO;
|
|
||||||
self.backspaceClearHighlighted = NO;
|
|
||||||
[self kb_hideBackspaceClearLabel];
|
|
||||||
// 移除旧按钮
|
// 移除旧按钮
|
||||||
for (UIView *row in @[self.row1, self.row2, self.row3, self.row4]) {
|
for (UIView *row in @[self.row1, self.row2, self.row3, self.row4]) {
|
||||||
[row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
|
[row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
|
||||||
@@ -406,16 +369,8 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier {
|
|||||||
[btn addTarget:self action:@selector(onKeyTapped:) forControlEvents:UIControlEventTouchUpInside];
|
[btn addTarget:self action:@selector(onKeyTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||||
[row addSubview:btn];
|
[row addSubview:btn];
|
||||||
|
|
||||||
// ⌫ 长按:开始连续逐个删除(无需 NSTimer)。使用 UILongPressGestureRecognizer 识别长按
|
|
||||||
if (key.type == KBKeyTypeBackspace) {
|
if (key.type == KBKeyTypeBackspace) {
|
||||||
UILongPressGestureRecognizer *lp =
|
[self.backspaceHandler bindDeleteButton:btn];
|
||||||
[[UILongPressGestureRecognizer alloc] initWithTarget:self
|
|
||||||
action:@selector(onBackspaceLongPress:)];
|
|
||||||
lp.minimumPressDuration = kKBBackspaceLongPressMinDuration;
|
|
||||||
lp.allowableMovement = CGFLOAT_MAX;
|
|
||||||
lp.cancelsTouchesInView = YES; // 被识别为长按时,取消普通点击
|
|
||||||
[btn addGestureRecognizer:lp];
|
|
||||||
self.backspaceButton = btn;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shift 按钮选中态随大小写状态变化
|
// Shift 按钮选中态随大小写状态变化
|
||||||
@@ -703,383 +658,6 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier {
|
|||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 长按退格:先连续单删,稍后切换为按段删除;松手停止。(不使用 NSTimer/DisplayLink)
|
|
||||||
- (void)onBackspaceLongPress:(UILongPressGestureRecognizer *)gr {
|
|
||||||
if (gr) {
|
|
||||||
self.backspaceLastTouchPointInSelf = [gr locationInView:self];
|
|
||||||
self.backspaceHasLastTouchPoint = YES;
|
|
||||||
}
|
|
||||||
switch (gr.state) {
|
|
||||||
case UIGestureRecognizerStateBegan: {
|
|
||||||
// 递增 token:使上一次长按(可能尚未触发的 dispatch_after)立即失效,
|
|
||||||
// 避免“第一次长按后第二次长按失效/异常加速”等并发问题。
|
|
||||||
self.backspaceHoldToken += 1;
|
|
||||||
NSUInteger token = self.backspaceHoldToken;
|
|
||||||
self.backspaceHoldActive = YES;
|
|
||||||
self.backspaceHoldStartTime = [NSDate date].timeIntervalSinceReferenceDate;
|
|
||||||
self.backspaceChunkModeActive = NO;
|
|
||||||
[self kb_setBackspaceClearHighlighted:NO];
|
|
||||||
[self kb_hideBackspaceClearLabel];
|
|
||||||
[self kb_backspaceStepForToken:token];
|
|
||||||
} break;
|
|
||||||
case UIGestureRecognizerStateChanged: {
|
|
||||||
[self kb_handleBackspaceLongPressChanged:gr];
|
|
||||||
} break;
|
|
||||||
case UIGestureRecognizerStateEnded:
|
|
||||||
case UIGestureRecognizerStateCancelled:
|
|
||||||
case UIGestureRecognizerStateFailed: {
|
|
||||||
[self kb_handleBackspaceLongPressEnded:gr];
|
|
||||||
} break;
|
|
||||||
default: break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - Helpers
|
|
||||||
|
|
||||||
// 单步删除并在需要时安排下一次,直到松手或无内容
|
|
||||||
- (void)kb_backspaceStepForToken:(NSUInteger)token {
|
|
||||||
if (!self.backspaceHoldActive) { return; }
|
|
||||||
if (token != self.backspaceHoldToken) { return; }
|
|
||||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
|
||||||
if (!ivc) { self.backspaceHoldActive = NO; return; }
|
|
||||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
|
||||||
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
|
||||||
NSTimeInterval elapsed = [NSDate date].timeIntervalSinceReferenceDate - self.backspaceHoldStartTime;
|
|
||||||
NSInteger deleteCount = 1;
|
|
||||||
if (before.length > 0) {
|
|
||||||
deleteCount = [self kb_backspaceDeleteCountForContext:before elapsed:elapsed];
|
|
||||||
}
|
|
||||||
if (!self.backspaceChunkModeActive && elapsed >= kKBBackspaceChunkStartDelay) {
|
|
||||||
self.backspaceChunkModeActive = YES;
|
|
||||||
[self kb_showBackspaceClearLabelIfNeeded];
|
|
||||||
}
|
|
||||||
for (NSInteger i = 0; i < deleteCount; i++) {
|
|
||||||
[proxy deleteBackward];
|
|
||||||
}
|
|
||||||
|
|
||||||
NSTimeInterval interval = [self kb_backspaceRepeatIntervalForElapsed:elapsed];
|
|
||||||
__weak typeof(self) weakSelf = self;
|
|
||||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
|
|
||||||
(int64_t)(interval * NSEC_PER_SEC)),
|
|
||||||
dispatch_get_main_queue(), ^{
|
|
||||||
__strong typeof(weakSelf) selfStrong = weakSelf;
|
|
||||||
[selfStrong kb_backspaceStepForToken:token];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSTimeInterval)kb_backspaceRepeatIntervalForElapsed:(NSTimeInterval)elapsed {
|
|
||||||
if (elapsed >= kKBBackspaceChunkStartDelay) {
|
|
||||||
return kKBBackspaceChunkRepeatInterval;
|
|
||||||
}
|
|
||||||
return kKBBackspaceRepeatInterval;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSInteger)kb_backspaceDeleteCountForContext:(NSString *)context elapsed:(NSTimeInterval)elapsed {
|
|
||||||
if (elapsed < kKBBackspaceChunkStartDelay) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
NSInteger maxCount = (elapsed >= kKBBackspaceChunkFastDelay)
|
|
||||||
? kKBBackspaceChunkSizeFast : kKBBackspaceChunkSize;
|
|
||||||
return [self kb_backspaceChunkDeleteCountForContext:context maxCount:maxCount];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSInteger)kb_backspaceChunkDeleteCountForContext:(NSString *)context maxCount:(NSInteger)maxCount {
|
|
||||||
if (context.length == 0) { return 1; }
|
|
||||||
|
|
||||||
static NSCharacterSet *whitespaceSet = nil;
|
|
||||||
static NSCharacterSet *asciiWordSet = nil;
|
|
||||||
static NSCharacterSet *punctuationSet = nil;
|
|
||||||
static dispatch_once_t onceToken;
|
|
||||||
dispatch_once(&onceToken, ^{
|
|
||||||
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
|
||||||
asciiWordSet = [NSCharacterSet characterSetWithCharactersInString:
|
|
||||||
@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"];
|
|
||||||
punctuationSet = [NSCharacterSet punctuationCharacterSet];
|
|
||||||
});
|
|
||||||
|
|
||||||
__block NSInteger deleteCount = 0;
|
|
||||||
__block KBBackspaceChunkClass chunkClass = KBBackspaceChunkClassUnknown;
|
|
||||||
[context enumerateSubstringsInRange:NSMakeRange(0, context.length)
|
|
||||||
options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse
|
|
||||||
usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) {
|
|
||||||
if (substring.length == 0) { return; }
|
|
||||||
KBBackspaceChunkClass currentClass = KBBackspaceChunkClassOther;
|
|
||||||
if ([substring rangeOfCharacterFromSet:whitespaceSet].location != NSNotFound) {
|
|
||||||
currentClass = KBBackspaceChunkClassWhitespace;
|
|
||||||
} else if ([substring rangeOfCharacterFromSet:asciiWordSet].location != NSNotFound) {
|
|
||||||
currentClass = KBBackspaceChunkClassASCIIWord;
|
|
||||||
} else if ([substring rangeOfCharacterFromSet:punctuationSet].location != NSNotFound) {
|
|
||||||
currentClass = KBBackspaceChunkClassPunctuation;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chunkClass == KBBackspaceChunkClassUnknown) {
|
|
||||||
chunkClass = currentClass;
|
|
||||||
} else if (chunkClass != currentClass) {
|
|
||||||
*stop = YES;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteCount += 1;
|
|
||||||
if (deleteCount >= maxCount) {
|
|
||||||
*stop = YES;
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
|
|
||||||
return MAX(deleteCount, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSInteger)kb_clearDeleteCountForContext:(NSString *)context
|
|
||||||
hitBoundary:(BOOL *)hitBoundary {
|
|
||||||
if (context.length == 0) { return kKBBackspaceClearBatchSize; }
|
|
||||||
|
|
||||||
static NSCharacterSet *sentenceBoundarySet = nil;
|
|
||||||
static NSCharacterSet *whitespaceSet = nil;
|
|
||||||
static dispatch_once_t onceToken;
|
|
||||||
dispatch_once(&onceToken, ^{
|
|
||||||
sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;。!?;…\n"];
|
|
||||||
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
|
||||||
});
|
|
||||||
|
|
||||||
NSInteger length = context.length;
|
|
||||||
NSInteger end = length;
|
|
||||||
while (end > 0) {
|
|
||||||
unichar ch = [context characterAtIndex:end - 1];
|
|
||||||
if ([whitespaceSet characterIsMember:ch]) {
|
|
||||||
end -= 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NSInteger searchEnd = end;
|
|
||||||
while (searchEnd > 0) {
|
|
||||||
unichar ch = [context characterAtIndex:searchEnd - 1];
|
|
||||||
if ([sentenceBoundarySet characterIsMember:ch]) {
|
|
||||||
searchEnd -= 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NSInteger boundaryIndex = NSNotFound;
|
|
||||||
for (NSInteger i = searchEnd - 1; i >= 0; i--) {
|
|
||||||
unichar ch = [context characterAtIndex:i];
|
|
||||||
if ([sentenceBoundarySet characterIsMember:ch]) {
|
|
||||||
boundaryIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BOOL boundaryFound = (boundaryIndex != NSNotFound);
|
|
||||||
NSInteger deleteCount = length;
|
|
||||||
if (boundaryIndex != NSNotFound) {
|
|
||||||
deleteCount = length - (boundaryIndex + 1);
|
|
||||||
}
|
|
||||||
deleteCount = MAX(deleteCount, 1);
|
|
||||||
if (hitBoundary) {
|
|
||||||
*hitBoundary = boundaryFound;
|
|
||||||
}
|
|
||||||
return MIN(deleteCount, kKBBackspaceClearMaxStep);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)kb_handleBackspaceLongPressChanged:(UILongPressGestureRecognizer *)gr {
|
|
||||||
if (!self.backspaceHoldActive) { return; }
|
|
||||||
NSTimeInterval elapsed = [NSDate date].timeIntervalSinceReferenceDate - self.backspaceHoldStartTime;
|
|
||||||
if (elapsed < kKBBackspaceChunkStartDelay) { return; }
|
|
||||||
[self kb_showBackspaceClearLabelIfNeeded];
|
|
||||||
CGPoint point = [gr locationInView:self];
|
|
||||||
self.backspaceLastTouchPointInSelf = point;
|
|
||||||
self.backspaceHasLastTouchPoint = YES;
|
|
||||||
BOOL inside = [self kb_isPointInsideBackspaceClearLabel:point];
|
|
||||||
[self kb_setBackspaceClearHighlighted:inside];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)kb_handleBackspaceLongPressEnded:(UILongPressGestureRecognizer *)gr {
|
|
||||||
BOOL shouldClear = self.backspaceClearHighlighted;
|
|
||||||
if (!shouldClear) {
|
|
||||||
CGPoint point = CGPointZero;
|
|
||||||
if (gr) {
|
|
||||||
point = [gr locationInView:self];
|
|
||||||
} else if (self.backspaceHasLastTouchPoint) {
|
|
||||||
point = self.backspaceLastTouchPointInSelf;
|
|
||||||
}
|
|
||||||
shouldClear = [self kb_isPointInsideBackspaceClearLabel:point];
|
|
||||||
}
|
|
||||||
self.backspaceHoldActive = NO;
|
|
||||||
self.backspaceChunkModeActive = NO;
|
|
||||||
self.backspaceHoldToken += 1; // 结束/取消时也使剩余回调失效
|
|
||||||
self.backspaceHasLastTouchPoint = NO;
|
|
||||||
[self kb_hideBackspaceClearLabel];
|
|
||||||
if (shouldClear) {
|
|
||||||
[self kb_clearAllInput];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)kb_showBackspaceClearLabelIfNeeded {
|
|
||||||
if (!self.backspaceButton) { return; }
|
|
||||||
UILabel *label = self.backspaceClearLabel;
|
|
||||||
[self kb_refreshBackspaceClearLabelColors];
|
|
||||||
if (!label.superview) {
|
|
||||||
[self addSubview:label];
|
|
||||||
}
|
|
||||||
[self kb_updateBackspaceClearLabelFrame];
|
|
||||||
[self bringSubviewToFront:label];
|
|
||||||
if (label.hidden) {
|
|
||||||
label.alpha = 0.0;
|
|
||||||
label.hidden = NO;
|
|
||||||
[self kb_playLightHaptic];
|
|
||||||
[UIView animateWithDuration:0.12 animations:^{
|
|
||||||
label.alpha = 1.0;
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)kb_hideBackspaceClearLabel {
|
|
||||||
if (!_backspaceClearLabel || _backspaceClearLabel.hidden) { return; }
|
|
||||||
_backspaceClearLabel.hidden = YES;
|
|
||||||
_backspaceClearLabel.alpha = 1.0;
|
|
||||||
[self kb_setBackspaceClearHighlighted:NO];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)kb_updateBackspaceClearLabelFrame {
|
|
||||||
if (!self.backspaceButton || !self.backspaceClearLabel) { return; }
|
|
||||||
CGRect btnFrame = [self.backspaceButton convertRect:self.backspaceButton.bounds toView:self];
|
|
||||||
UILabel *label = self.backspaceClearLabel;
|
|
||||||
CGSize textSize = [label sizeThatFits:CGSizeMake(CGFLOAT_MAX, kKBBackspaceClearLabelHeight)];
|
|
||||||
CGFloat width = MAX(textSize.width + kKBBackspaceClearLabelPaddingX * 2.0, 60.0);
|
|
||||||
CGFloat height = kKBBackspaceClearLabelHeight;
|
|
||||||
CGFloat x = CGRectGetMidX(btnFrame) - width * 0.5;
|
|
||||||
CGFloat y = CGRectGetMinY(btnFrame) - height - kKBBackspaceClearLabelTopGap;
|
|
||||||
if (x < kKBRowHorizontalInset) { x = kKBRowHorizontalInset; }
|
|
||||||
if (x + width > CGRectGetWidth(self.bounds) - kKBRowHorizontalInset) {
|
|
||||||
x = CGRectGetWidth(self.bounds) - kKBRowHorizontalInset - width;
|
|
||||||
}
|
|
||||||
if (y < 0) { y = 0; }
|
|
||||||
label.frame = CGRectIntegral(CGRectMake(x, y, width, height));
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)kb_isPointInsideBackspaceClearLabel:(CGPoint)point {
|
|
||||||
if (!self.backspaceClearLabel || self.backspaceClearLabel.hidden) { return NO; }
|
|
||||||
[self kb_updateBackspaceClearLabelFrame];
|
|
||||||
CGRect hitFrame = CGRectInset(self.backspaceClearLabel.frame, -12.0, -10.0);
|
|
||||||
return CGRectContainsPoint(hitFrame, point);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)kb_setBackspaceClearHighlighted:(BOOL)highlighted {
|
|
||||||
if (self.backspaceClearHighlighted == highlighted) { return; }
|
|
||||||
self.backspaceClearHighlighted = highlighted;
|
|
||||||
[self kb_refreshBackspaceClearLabelColors];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)kb_refreshBackspaceClearLabelColors {
|
|
||||||
UILabel *label = self.backspaceClearLabel;
|
|
||||||
label.textColor = [KBSkinManager shared].current.keyTextColor ?: UIColor.blackColor;
|
|
||||||
label.backgroundColor = self.backspaceClearHighlighted
|
|
||||||
? [self kb_backspaceClearLabelHighlightedColor]
|
|
||||||
: [self kb_backspaceClearLabelNormalColor];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (UIColor *)kb_backspaceClearLabelNormalColor {
|
|
||||||
KBSkinTheme *t = [KBSkinManager shared].current;
|
|
||||||
return t.keyHighlightBackground ?: [UIColor colorWithWhite:0.9 alpha:1.0];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (UIColor *)kb_backspaceClearLabelHighlightedColor {
|
|
||||||
KBSkinTheme *t = [KBSkinManager shared].current;
|
|
||||||
return t.accentColor ?: t.keyHighlightBackground ?: [UIColor colorWithWhite:0.8 alpha:1.0];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)kb_playLightHaptic {
|
|
||||||
if (@available(iOS 10.0, *)) {
|
|
||||||
UIImpactFeedbackGenerator *gen = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
|
|
||||||
[gen prepare];
|
|
||||||
[gen impactOccurred];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (UILabel *)backspaceClearLabel {
|
|
||||||
if (!_backspaceClearLabel) {
|
|
||||||
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
|
|
||||||
label.text = @"立刻清空";
|
|
||||||
label.textAlignment = NSTextAlignmentCenter;
|
|
||||||
label.font = [UIFont systemFontOfSize:12 weight:UIFontWeightSemibold];
|
|
||||||
label.textColor = [KBSkinManager shared].current.keyTextColor ?: UIColor.blackColor;
|
|
||||||
label.backgroundColor = [self kb_backspaceClearLabelNormalColor];
|
|
||||||
label.layer.cornerRadius = kKBBackspaceClearLabelCornerRadius;
|
|
||||||
label.layer.masksToBounds = YES;
|
|
||||||
label.hidden = YES;
|
|
||||||
label.userInteractionEnabled = NO;
|
|
||||||
_backspaceClearLabel = label;
|
|
||||||
}
|
|
||||||
return _backspaceClearLabel;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)kb_clearAllInput {
|
|
||||||
self.backspaceClearToken += 1;
|
|
||||||
NSUInteger token = self.backspaceClearToken;
|
|
||||||
[self kb_clearAllInputStepForToken:token guard:0 emptyRounds:0];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)kb_clearAllInputStepForToken:(NSUInteger)token
|
|
||||||
guard:(NSInteger)guard
|
|
||||||
emptyRounds:(NSInteger)emptyRounds {
|
|
||||||
if (token != self.backspaceClearToken) { return; }
|
|
||||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
|
||||||
if (!ivc) { return; }
|
|
||||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
|
||||||
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
|
||||||
NSInteger count = before.length;
|
|
||||||
NSInteger batch = 0;
|
|
||||||
NSInteger nextEmptyRounds = emptyRounds;
|
|
||||||
BOOL hitBoundary = NO;
|
|
||||||
if (count > 0) {
|
|
||||||
batch = [self kb_clearDeleteCountForContext:before hitBoundary:&hitBoundary];
|
|
||||||
nextEmptyRounds = 0;
|
|
||||||
} else {
|
|
||||||
batch = kKBBackspaceClearBatchSize;
|
|
||||||
nextEmptyRounds = emptyRounds + 1;
|
|
||||||
}
|
|
||||||
if (batch <= 0) { batch = 1; }
|
|
||||||
|
|
||||||
if (guard >= kKBBackspaceClearMaxDeletes ||
|
|
||||||
nextEmptyRounds > kKBBackspaceClearEmptyContextMaxRounds) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (NSInteger i = 0; i < batch; i++) {
|
|
||||||
[proxy deleteBackward];
|
|
||||||
}
|
|
||||||
|
|
||||||
NSInteger nextGuard = guard + batch;
|
|
||||||
BOOL shouldContinue = NO;
|
|
||||||
if (count > 0 && !hitBoundary) {
|
|
||||||
if (count > batch) {
|
|
||||||
shouldContinue = YES;
|
|
||||||
} else if ([proxy hasText]) {
|
|
||||||
shouldContinue = YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shouldContinue) { return; }
|
|
||||||
__weak typeof(self) weakSelf = self;
|
|
||||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
|
|
||||||
(int64_t)(kKBBackspaceClearBatchInterval * NSEC_PER_SEC)),
|
|
||||||
dispatch_get_main_queue(), ^{
|
|
||||||
__strong typeof(weakSelf) selfStrong = weakSelf;
|
|
||||||
[selfStrong kb_clearAllInputStepForToken:token
|
|
||||||
guard:nextGuard
|
|
||||||
emptyRounds:nextEmptyRounds];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - Layout
|
|
||||||
|
|
||||||
- (void)layoutSubviews {
|
|
||||||
[super layoutSubviews];
|
|
||||||
if (self.backspaceClearLabel && !self.backspaceClearLabel.hidden) {
|
|
||||||
[self kb_updateBackspaceClearLabelFrame];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - Lazy
|
#pragma mark - Lazy
|
||||||
|
|
||||||
- (UIView *)row1 { if (!_row1) _row1 = [UIView new]; return _row1; }
|
- (UIView *)row1 { if (!_row1) _row1 = [UIView new]; return _row1; }
|
||||||
|
|||||||
@@ -178,6 +178,7 @@
|
|||||||
04FC956A2EB05497007BD342 /* KBKeyButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95692EB05497007BD342 /* KBKeyButton.m */; };
|
04FC956A2EB05497007BD342 /* KBKeyButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95692EB05497007BD342 /* KBKeyButton.m */; };
|
||||||
04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC956C2EB054B7007BD342 /* KBKeyboardView.m */; };
|
04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC956C2EB054B7007BD342 /* KBKeyboardView.m */; };
|
||||||
04FC95702EB09516007BD342 /* KBFunctionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC956F2EB09516007BD342 /* KBFunctionView.m */; };
|
04FC95702EB09516007BD342 /* KBFunctionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC956F2EB09516007BD342 /* KBFunctionView.m */; };
|
||||||
|
A1B2C9032FBD000100000001 /* KBBackspaceLongPressHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C9022FBD000100000001 /* KBBackspaceLongPressHandler.m */; };
|
||||||
04FC95732EB09570007BD342 /* KBFunctionBarView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95722EB09570007BD342 /* KBFunctionBarView.m */; };
|
04FC95732EB09570007BD342 /* KBFunctionBarView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95722EB09570007BD342 /* KBFunctionBarView.m */; };
|
||||||
04FC95762EB095DE007BD342 /* KBFunctionPasteView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95752EB095DE007BD342 /* KBFunctionPasteView.m */; };
|
04FC95762EB095DE007BD342 /* KBFunctionPasteView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95752EB095DE007BD342 /* KBFunctionPasteView.m */; };
|
||||||
04FC95792EB09BC8007BD342 /* KBKeyBoardMainView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95782EB09BC8007BD342 /* KBKeyBoardMainView.m */; };
|
04FC95792EB09BC8007BD342 /* KBKeyBoardMainView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95782EB09BC8007BD342 /* KBKeyBoardMainView.m */; };
|
||||||
@@ -538,6 +539,8 @@
|
|||||||
04FC956C2EB054B7007BD342 /* KBKeyboardView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardView.m; sourceTree = "<group>"; };
|
04FC956C2EB054B7007BD342 /* KBKeyboardView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBKeyboardView.m; sourceTree = "<group>"; };
|
||||||
04FC956E2EB09516007BD342 /* KBFunctionView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFunctionView.h; sourceTree = "<group>"; };
|
04FC956E2EB09516007BD342 /* KBFunctionView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFunctionView.h; sourceTree = "<group>"; };
|
||||||
04FC956F2EB09516007BD342 /* KBFunctionView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFunctionView.m; sourceTree = "<group>"; };
|
04FC956F2EB09516007BD342 /* KBFunctionView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFunctionView.m; sourceTree = "<group>"; };
|
||||||
|
A1B2C9012FBD000100000001 /* KBBackspaceLongPressHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBBackspaceLongPressHandler.h; sourceTree = "<group>"; };
|
||||||
|
A1B2C9022FBD000100000001 /* KBBackspaceLongPressHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBBackspaceLongPressHandler.m; sourceTree = "<group>"; };
|
||||||
04FC95712EB09570007BD342 /* KBFunctionBarView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFunctionBarView.h; sourceTree = "<group>"; };
|
04FC95712EB09570007BD342 /* KBFunctionBarView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFunctionBarView.h; sourceTree = "<group>"; };
|
||||||
04FC95722EB09570007BD342 /* KBFunctionBarView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFunctionBarView.m; sourceTree = "<group>"; };
|
04FC95722EB09570007BD342 /* KBFunctionBarView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFunctionBarView.m; sourceTree = "<group>"; };
|
||||||
04FC95742EB095DE007BD342 /* KBFunctionPasteView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFunctionPasteView.h; sourceTree = "<group>"; };
|
04FC95742EB095DE007BD342 /* KBFunctionPasteView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFunctionPasteView.h; sourceTree = "<group>"; };
|
||||||
@@ -805,6 +808,8 @@
|
|||||||
0477BD942EBAFF4E0055D639 /* Utils */ = {
|
0477BD942EBAFF4E0055D639 /* Utils */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
A1B2C9012FBD000100000001 /* KBBackspaceLongPressHandler.h */,
|
||||||
|
A1B2C9022FBD000100000001 /* KBBackspaceLongPressHandler.m */,
|
||||||
);
|
);
|
||||||
path = Utils;
|
path = Utils;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1892,6 +1897,7 @@
|
|||||||
049FB23C2EC4766700FAB05D /* KBStreamOverlayView.m in Sources */,
|
049FB23C2EC4766700FAB05D /* KBStreamOverlayView.m in Sources */,
|
||||||
049FB22F2EC34EB900FAB05D /* KBStreamTextView.m in Sources */,
|
049FB22F2EC34EB900FAB05D /* KBStreamTextView.m in Sources */,
|
||||||
04FC95702EB09516007BD342 /* KBFunctionView.m in Sources */,
|
04FC95702EB09516007BD342 /* KBFunctionView.m in Sources */,
|
||||||
|
A1B2C9032FBD000100000001 /* KBBackspaceLongPressHandler.m in Sources */,
|
||||||
049FB23F2EC4B6EF00FAB05D /* KBULBridgeNotification.m in Sources */,
|
049FB23F2EC4B6EF00FAB05D /* KBULBridgeNotification.m in Sources */,
|
||||||
04791F992ED49CE7004E8522 /* KBFont.m in Sources */,
|
04791F992ED49CE7004E8522 /* KBFont.m in Sources */,
|
||||||
04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */,
|
04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */,
|
||||||
|
|||||||
Reference in New Issue
Block a user