From 9544ad75ffb478f0260faa3bdeea348f201670de Mon Sep 17 00:00:00 2001 From: CodeST <694468528@qq.com> Date: Fri, 19 Dec 2025 18:45:14 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E9=94=AE=E7=9B=98=E9=95=BF?= =?UTF-8?q?=E6=8C=89=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Utils/KBBackspaceLongPressHandler.h | 22 + .../Utils/KBBackspaceLongPressHandler.m | 475 ++++++++++++++++++ CustomKeyboard/View/KBFunctionView.m | 21 +- CustomKeyboard/View/KBKeyboardView.m | 432 +--------------- keyBoard.xcodeproj/project.pbxproj | 6 + 5 files changed, 514 insertions(+), 442 deletions(-) create mode 100644 CustomKeyboard/Utils/KBBackspaceLongPressHandler.h create mode 100644 CustomKeyboard/Utils/KBBackspaceLongPressHandler.m diff --git a/CustomKeyboard/Utils/KBBackspaceLongPressHandler.h b/CustomKeyboard/Utils/KBBackspaceLongPressHandler.h new file mode 100644 index 0000000..8fe22a4 --- /dev/null +++ b/CustomKeyboard/Utils/KBBackspaceLongPressHandler.h @@ -0,0 +1,22 @@ +// +// KBBackspaceLongPressHandler.h +// CustomKeyboard +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface KBBackspaceLongPressHandler : NSObject + +- (instancetype)initWithContainerView:(UIView *)containerView; + +/// 配置删除按钮(包含长按删除与“立刻清空”提示) +- (void)bindDeleteButton:(nullable UIView *)button; + +/// 触发“立刻清空”逻辑(可用于功能面板的清空按钮) +- (void)performClearAction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CustomKeyboard/Utils/KBBackspaceLongPressHandler.m b/CustomKeyboard/Utils/KBBackspaceLongPressHandler.m new file mode 100644 index 0000000..70c17ac --- /dev/null +++ b/CustomKeyboard/Utils/KBBackspaceLongPressHandler.m @@ -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 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 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 diff --git a/CustomKeyboard/View/KBFunctionView.m b/CustomKeyboard/View/KBFunctionView.m index 5f6b0fc..9a9d19d 100644 --- a/CustomKeyboard/View/KBFunctionView.m +++ b/CustomKeyboard/View/KBFunctionView.m @@ -25,6 +25,7 @@ #import "KBTagItemModel.h" #import #import "KBBizCode.h" +#import "KBBackspaceLongPressHandler.h" @interface KBFunctionView () // UI @@ -59,6 +60,7 @@ // UL 双路兜底 @property (nonatomic, assign) NSUInteger kb_ulSeq; // 当前 UL 发起序号 @property (nonatomic, assign) BOOL kb_ulHandledFlag; // 主 App 已确认处理 UL +@property (nonatomic, strong) KBBackspaceLongPressHandler *backspaceHandler; @end @implementation KBFunctionView @@ -67,6 +69,7 @@ if (self = [super initWithFrame:frame]) { // 背景使用当前主题强调色 [self kb_applyTheme]; + self.backspaceHandler = [[KBBackspaceLongPressHandler alloc] initWithContainerView:self]; [self setupUI]; // [self reloadDemoData]; @@ -770,22 +773,9 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C id proxy = ivc.textDocumentProxy; [proxy deleteBackward]; } - - (void)onTapClear { +- (void)onTapClear { NSLog(@"点击:清空"); - // 连续删除:仅清空光标之前的输入(不改动 pasteView 的内容) - UIInputViewController *ivc = KBFindInputViewController(self); - id 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; - } + [self.backspaceHandler performClearAction]; } - (void)onTapSend { NSLog(@"点击:发送"); @@ -872,6 +862,7 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C [_deleteButtonInternal setImage:[UIImage imageNamed:@"kb_del_icon"] forState:UIControlStateNormal]; [_deleteButtonInternal addTarget:self action:@selector(onTapDelete) forControlEvents:UIControlEventTouchUpInside]; + [self.backspaceHandler bindDeleteButton:_deleteButtonInternal]; } return _deleteButtonInternal; } diff --git a/CustomKeyboard/View/KBKeyboardView.m b/CustomKeyboard/View/KBKeyboardView.m index c9695f8..c77767f 100644 --- a/CustomKeyboard/View/KBKeyboardView.m +++ b/CustomKeyboard/View/KBKeyboardView.m @@ -6,32 +6,15 @@ #import "KBKeyboardView.h" #import "KBKeyButton.h" #import "KBKey.h" -#import "KBResponderUtils.h" // 封装的响应链工具 #import "KBSkinManager.h" #import "KBKeyPreviewView.h" +#import "KBBackspaceLongPressHandler.h" // UI 常量统一管理,方便后续调试样式(以 375 宽设计稿为基准,通过 KBFit 做等比缩放) #define kKBRowVerticalSpacing KBFit(8.0f) #define kKBRowHorizontalInset KBFit(6.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 kKBPreviewHideDuration = 0.06; @@ -42,31 +25,13 @@ static const CGFloat kKBSpaceWidthMultiplier = 3.0; // 第二行字母行的左右占位比例(用于居中) static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5; -typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { - KBBackspaceChunkClassUnknown = 0, - KBBackspaceChunkClassWhitespace, - KBBackspaceChunkClassASCIIWord, - KBBackspaceChunkClassPunctuation, - KBBackspaceChunkClassOther -}; - @interface KBKeyboardView () @property (nonatomic, strong) UIView *row1; @property (nonatomic, strong) UIView *row2; @property (nonatomic, strong) UIView *row3; @property (nonatomic, strong) UIView *row4; @property (nonatomic, strong) NSArray *> *keysForRows; -// 长按退格的一次次删除控制标记(不使用 NSTimer,仅用 GCD 递归调度) -@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) KBBackspaceLongPressHandler *backspaceHandler; @property (nonatomic, strong) KBKeyPreviewView *previewView; @end @@ -79,6 +44,7 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { // 默认小写:与需求一致,初始不开启 Shift _shiftOn = NO; _symbolsMoreOn = NO; // 数字面板默认第一页(123) + self.backspaceHandler = [[KBBackspaceLongPressHandler alloc] initWithContainerView:self]; [self buildBase]; [self reloadKeys]; } @@ -127,10 +93,7 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { #pragma mark - Public - (void)reloadKeys { - self.backspaceButton = nil; - self.backspaceChunkModeActive = NO; - self.backspaceClearHighlighted = NO; - [self kb_hideBackspaceClearLabel]; + [self.backspaceHandler bindDeleteButton:nil]; // 移除旧按钮 for (UIView *row in @[self.row1, self.row2, self.row3, self.row4]) { [row.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; @@ -406,16 +369,8 @@ edgeSpacerMultiplier:(CGFloat)edgeSpacerMultiplier { [btn addTarget:self action:@selector(onKeyTapped:) forControlEvents:UIControlEventTouchUpInside]; [row addSubview:btn]; - // ⌫ 长按:开始连续逐个删除(无需 NSTimer)。使用 UILongPressGestureRecognizer 识别长按 if (key.type == KBKeyTypeBackspace) { - UILongPressGestureRecognizer *lp = - [[UILongPressGestureRecognizer alloc] initWithTarget:self - action:@selector(onBackspaceLongPress:)]; - lp.minimumPressDuration = kKBBackspaceLongPressMinDuration; - lp.allowableMovement = CGFLOAT_MAX; - lp.cancelsTouchesInView = YES; // 被识别为长按时,取消普通点击 - [btn addGestureRecognizer:lp]; - self.backspaceButton = btn; + [self.backspaceHandler bindDeleteButton:btn]; } // 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 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 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 - (UIView *)row1 { if (!_row1) _row1 = [UIView new]; return _row1; } diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index 222ba43..149ec77 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -178,6 +178,7 @@ 04FC956A2EB05497007BD342 /* KBKeyButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95692EB05497007BD342 /* KBKeyButton.m */; }; 04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC956C2EB054B7007BD342 /* KBKeyboardView.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 */; }; 04FC95762EB095DE007BD342 /* KBFunctionPasteView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC95752EB095DE007BD342 /* KBFunctionPasteView.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 = ""; }; 04FC956E2EB09516007BD342 /* KBFunctionView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFunctionView.h; sourceTree = ""; }; 04FC956F2EB09516007BD342 /* KBFunctionView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFunctionView.m; sourceTree = ""; }; + A1B2C9012FBD000100000001 /* KBBackspaceLongPressHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBBackspaceLongPressHandler.h; sourceTree = ""; }; + A1B2C9022FBD000100000001 /* KBBackspaceLongPressHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBBackspaceLongPressHandler.m; sourceTree = ""; }; 04FC95712EB09570007BD342 /* KBFunctionBarView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFunctionBarView.h; sourceTree = ""; }; 04FC95722EB09570007BD342 /* KBFunctionBarView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFunctionBarView.m; sourceTree = ""; }; 04FC95742EB095DE007BD342 /* KBFunctionPasteView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFunctionPasteView.h; sourceTree = ""; }; @@ -805,6 +808,8 @@ 0477BD942EBAFF4E0055D639 /* Utils */ = { isa = PBXGroup; children = ( + A1B2C9012FBD000100000001 /* KBBackspaceLongPressHandler.h */, + A1B2C9022FBD000100000001 /* KBBackspaceLongPressHandler.m */, ); path = Utils; sourceTree = ""; @@ -1892,6 +1897,7 @@ 049FB23C2EC4766700FAB05D /* KBStreamOverlayView.m in Sources */, 049FB22F2EC34EB900FAB05D /* KBStreamTextView.m in Sources */, 04FC95702EB09516007BD342 /* KBFunctionView.m in Sources */, + A1B2C9032FBD000100000001 /* KBBackspaceLongPressHandler.m in Sources */, 049FB23F2EC4B6EF00FAB05D /* KBULBridgeNotification.m in Sources */, 04791F992ED49CE7004E8522 /* KBFont.m in Sources */, 04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */,