2025-12-19 18:45:14 +08:00
|
|
|
|
//
|
|
|
|
|
|
// KBBackspaceLongPressHandler.m
|
|
|
|
|
|
// CustomKeyboard
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
|
|
#import "KBBackspaceLongPressHandler.h"
|
|
|
|
|
|
#import "KBResponderUtils.h"
|
|
|
|
|
|
#import "KBSkinManager.h"
|
2025-12-19 19:21:08 +08:00
|
|
|
|
#import "KBBackspaceUndoManager.h"
|
2025-12-26 11:47:44 +08:00
|
|
|
|
#import "KBInputBufferManager.h"
|
2025-12-19 18:45:14 +08:00
|
|
|
|
|
|
|
|
|
|
static const NSTimeInterval kKBBackspaceLongPressMinDuration = 0.35;
|
|
|
|
|
|
static const NSTimeInterval kKBBackspaceRepeatInterval = 0.06;
|
2025-12-26 13:55:07 +08:00
|
|
|
|
static const NSTimeInterval kKBBackspaceChunkStartDelay = 0.6;
|
2025-12-19 18:45:14 +08:00
|
|
|
|
static const NSTimeInterval kKBBackspaceChunkRepeatInterval = 0.1;
|
2025-12-26 13:55:07 +08:00
|
|
|
|
static const NSTimeInterval kKBBackspaceChunkFastDelay = 1.2;
|
|
|
|
|
|
static const NSInteger kKBBackspaceChunkSize = 8;
|
|
|
|
|
|
static const NSInteger kKBBackspaceChunkSizeFast = 16;
|
2025-12-19 18:45:14 +08:00
|
|
|
|
static const CGFloat kKBBackspaceClearLabelCornerRadius = 8.0;
|
2025-12-26 14:19:39 +08:00
|
|
|
|
static const CGFloat kKBBackspaceClearLabelHeight = 34;
|
2025-12-19 18:45:14 +08:00
|
|
|
|
static const CGFloat kKBBackspaceClearLabelPaddingX = 10.0;
|
|
|
|
|
|
static const CGFloat kKBBackspaceClearLabelTopGap = 6.0;
|
|
|
|
|
|
static const CGFloat kKBBackspaceClearLabelHorizontalInset = 6.0;
|
2025-12-26 11:47:44 +08:00
|
|
|
|
static const NSTimeInterval kKBBackspaceClearBatchInterval = 0.02;
|
2025-12-19 18:45:14 +08:00
|
|
|
|
static const NSInteger kKBBackspaceClearMaxDeletes = 10000;
|
|
|
|
|
|
static const NSInteger kKBBackspaceClearEmptyContextMaxRounds = 40;
|
|
|
|
|
|
static const NSInteger kKBBackspaceClearMaxStep = 80;
|
2025-12-26 11:47:44 +08:00
|
|
|
|
static const NSInteger kKBBackspaceClearDeletesPerTick = 10;
|
2025-12-19 18:45:14 +08:00
|
|
|
|
|
|
|
|
|
|
typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|
|
|
|
|
KBBackspaceChunkClassUnknown = 0,
|
|
|
|
|
|
KBBackspaceChunkClassWhitespace,
|
|
|
|
|
|
KBBackspaceChunkClassASCIIWord,
|
|
|
|
|
|
KBBackspaceChunkClassPunctuation,
|
|
|
|
|
|
KBBackspaceChunkClassOther
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-26 11:47:44 +08:00
|
|
|
|
typedef NS_ENUM(NSInteger, KBClearPhase) {
|
|
|
|
|
|
KBClearPhaseSkipWhitespace = 0,
|
|
|
|
|
|
KBClearPhaseSkipTrailingBoundary,
|
|
|
|
|
|
KBClearPhaseDeleteUntilBoundary
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-19 18:45:14 +08:00
|
|
|
|
@interface KBBackspaceLongPressHandler ()
|
|
|
|
|
|
@property (nonatomic, weak) UIView *containerView;
|
|
|
|
|
|
@property (nonatomic, weak) UIView *backspaceButton;
|
|
|
|
|
|
@property (nonatomic, strong) UILongPressGestureRecognizer *longPress;
|
2025-12-19 19:56:39 +08:00
|
|
|
|
@property (nonatomic, assign) BOOL showClearLabelEnabled;
|
2025-12-19 18:45:14 +08:00
|
|
|
|
@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;
|
2025-12-23 18:05:01 +08:00
|
|
|
|
@property (nonatomic, copy) NSString *pendingClearBefore;
|
|
|
|
|
|
@property (nonatomic, copy) NSString *pendingClearAfter;
|
2025-12-26 11:47:44 +08:00
|
|
|
|
@property (nonatomic, assign) KBClearPhase backspaceClearPhase;
|
2025-12-19 18:45:14 +08:00
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
|
|
@implementation KBBackspaceLongPressHandler
|
|
|
|
|
|
|
|
|
|
|
|
- (instancetype)initWithContainerView:(UIView *)containerView {
|
|
|
|
|
|
if (self = [super init]) {
|
|
|
|
|
|
_containerView = containerView;
|
2025-12-26 11:47:44 +08:00
|
|
|
|
_backspaceClearPhase = KBClearPhaseSkipWhitespace;
|
2025-12-19 18:45:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
return self;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-19 19:56:39 +08:00
|
|
|
|
- (void)bindDeleteButton:(UIView *)button showClearLabel:(BOOL)showClearLabel {
|
2025-12-19 18:45:14 +08:00
|
|
|
|
if (self.backspaceButton == button) { return; }
|
|
|
|
|
|
|
|
|
|
|
|
if (self.longPress && self.backspaceButton) {
|
|
|
|
|
|
[self.backspaceButton removeGestureRecognizer:self.longPress];
|
|
|
|
|
|
}
|
|
|
|
|
|
self.backspaceButton = button;
|
2025-12-19 19:56:39 +08:00
|
|
|
|
self.showClearLabelEnabled = showClearLabel;
|
2025-12-19 18:45:14 +08:00
|
|
|
|
self.backspaceHoldActive = NO;
|
|
|
|
|
|
self.backspaceChunkModeActive = NO;
|
|
|
|
|
|
self.backspaceClearHighlighted = NO;
|
|
|
|
|
|
self.backspaceHasLastTouchPoint = NO;
|
|
|
|
|
|
self.backspaceHoldToken += 1;
|
|
|
|
|
|
[self kb_hideBackspaceClearLabel];
|
2025-12-23 18:05:01 +08:00
|
|
|
|
self.pendingClearBefore = nil;
|
|
|
|
|
|
self.pendingClearAfter = nil;
|
2025-12-19 18:45:14 +08:00
|
|
|
|
|
|
|
|
|
|
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: {
|
2025-12-26 11:47:44 +08:00
|
|
|
|
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
|
|
|
|
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
|
|
|
|
|
if (ivc) {
|
|
|
|
|
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
|
|
|
|
|
[[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy];
|
|
|
|
|
|
[[KBInputBufferManager shared] prepareSnapshotForDeleteWithContextBefore:proxy.documentContextBeforeInput
|
|
|
|
|
|
after:proxy.documentContextAfterInput];
|
|
|
|
|
|
}
|
2025-12-23 18:05:01 +08:00
|
|
|
|
if (self.showClearLabelEnabled) {
|
|
|
|
|
|
[self kb_capturePendingClearSnapshotIfNeeded];
|
2025-12-26 11:47:44 +08:00
|
|
|
|
[[KBInputBufferManager shared] beginPendingClearSnapshot];
|
2025-12-23 18:05:01 +08:00
|
|
|
|
}
|
2025-12-19 18:45:14 +08:00
|
|
|
|
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];
|
2025-12-19 19:56:39 +08:00
|
|
|
|
if (self.showClearLabelEnabled) {
|
|
|
|
|
|
[self kb_showBackspaceClearLabelIfNeeded];
|
|
|
|
|
|
}
|
2025-12-19 18:45:14 +08:00
|
|
|
|
[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 ?: @"";
|
2025-12-26 11:47:44 +08:00
|
|
|
|
if (before.length == 0) { before = [KBInputBufferManager shared].liveText ?: @""; }
|
2025-12-19 18:45:14 +08:00
|
|
|
|
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;
|
2025-12-19 19:56:39 +08:00
|
|
|
|
if (self.showClearLabelEnabled) {
|
|
|
|
|
|
[self kb_showBackspaceClearLabelIfNeeded];
|
|
|
|
|
|
}
|
2025-12-19 18:45:14 +08:00
|
|
|
|
}
|
2025-12-26 11:47:44 +08:00
|
|
|
|
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:(NSUInteger)deleteCount];
|
|
|
|
|
|
[[KBInputBufferManager shared] applyHoldDeleteCount:(NSUInteger)deleteCount];
|
2025-12-19 18:45:14 +08:00
|
|
|
|
|
|
|
|
|
|
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_"];
|
2025-12-26 13:55:07 +08:00
|
|
|
|
NSMutableCharacterSet *punct = [[NSCharacterSet punctuationCharacterSet] mutableCopy];
|
|
|
|
|
|
// 补齐常见中文/全角标点(避免 chunk 总是只删 1 个符号)
|
|
|
|
|
|
[punct addCharactersInString:@",。!?;:、()【】《》“”‘’·…—"];
|
|
|
|
|
|
punctuationSet = [punct copy];
|
2025-12-19 18:45:14 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
__block NSInteger deleteCount = 0;
|
2025-12-26 13:55:07 +08:00
|
|
|
|
typedef NS_ENUM(NSInteger, KBBackspaceChunkPhase) {
|
|
|
|
|
|
KBBackspaceChunkPhaseWhitespace = 0,
|
|
|
|
|
|
KBBackspaceChunkPhasePunctuation,
|
|
|
|
|
|
KBBackspaceChunkPhaseCore
|
|
|
|
|
|
};
|
|
|
|
|
|
__block KBBackspaceChunkPhase phase = KBBackspaceChunkPhaseWhitespace;
|
|
|
|
|
|
__block KBBackspaceChunkClass coreClass = KBBackspaceChunkClassUnknown;
|
|
|
|
|
|
|
2025-12-19 18:45:14 +08:00
|
|
|
|
[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; }
|
2025-12-26 13:55:07 +08:00
|
|
|
|
if (deleteCount >= maxCount) {
|
|
|
|
|
|
*stop = YES;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-19 18:45:14 +08:00
|
|
|
|
KBBackspaceChunkClass currentClass = KBBackspaceChunkClassOther;
|
|
|
|
|
|
if ([substring rangeOfCharacterFromSet:whitespaceSet].location != NSNotFound) {
|
|
|
|
|
|
currentClass = KBBackspaceChunkClassWhitespace;
|
|
|
|
|
|
} else if ([substring rangeOfCharacterFromSet:punctuationSet].location != NSNotFound) {
|
|
|
|
|
|
currentClass = KBBackspaceChunkClassPunctuation;
|
2025-12-26 13:55:07 +08:00
|
|
|
|
} else if ([substring rangeOfCharacterFromSet:asciiWordSet].location != NSNotFound) {
|
|
|
|
|
|
currentClass = KBBackspaceChunkClassASCIIWord;
|
2025-12-19 18:45:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 13:55:07 +08:00
|
|
|
|
BOOL consumed = NO;
|
|
|
|
|
|
while (!consumed) {
|
|
|
|
|
|
if (phase == KBBackspaceChunkPhaseWhitespace) {
|
|
|
|
|
|
if (currentClass == KBBackspaceChunkClassWhitespace) {
|
|
|
|
|
|
deleteCount += 1;
|
|
|
|
|
|
consumed = YES;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
phase = KBBackspaceChunkPhasePunctuation;
|
|
|
|
|
|
}
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (phase == KBBackspaceChunkPhasePunctuation) {
|
|
|
|
|
|
if (currentClass == KBBackspaceChunkClassPunctuation) {
|
|
|
|
|
|
deleteCount += 1;
|
|
|
|
|
|
consumed = YES;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
phase = KBBackspaceChunkPhaseCore;
|
|
|
|
|
|
}
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// phase == Core:连续删同一类(ASCII 单词 / 其它),让效果更像微信“几个字一组”
|
|
|
|
|
|
if (coreClass == KBBackspaceChunkClassUnknown) {
|
|
|
|
|
|
coreClass = currentClass;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (currentClass != coreClass) {
|
|
|
|
|
|
*stop = YES;
|
|
|
|
|
|
consumed = YES;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
deleteCount += 1;
|
|
|
|
|
|
consumed = YES;
|
2025-12-19 18:45:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (deleteCount >= maxCount) {
|
|
|
|
|
|
*stop = YES;
|
2025-12-26 13:55:07 +08:00
|
|
|
|
return;
|
2025-12-19 18:45:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
|
|
return MAX(deleteCount, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (NSInteger)kb_clearDeleteCountForContext:(NSString *)context
|
|
|
|
|
|
hitBoundary:(BOOL *)hitBoundary {
|
2025-12-26 11:47:44 +08:00
|
|
|
|
if (context.length == 0) {
|
|
|
|
|
|
if (hitBoundary) { *hitBoundary = NO; }
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2025-12-19 18:45:14 +08:00
|
|
|
|
|
|
|
|
|
|
static NSCharacterSet *sentenceBoundarySet = nil;
|
|
|
|
|
|
static NSCharacterSet *whitespaceSet = nil;
|
|
|
|
|
|
static dispatch_once_t onceToken;
|
|
|
|
|
|
dispatch_once(&onceToken, ^{
|
2025-12-26 11:47:44 +08:00
|
|
|
|
sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?。!?"];
|
2025-12-19 18:45:14 +08:00
|
|
|
|
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; }
|
2025-12-19 19:56:39 +08:00
|
|
|
|
if (!self.showClearLabelEnabled) { return; }
|
2025-12-19 18:45:14 +08:00
|
|
|
|
[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 {
|
2025-12-19 19:56:39 +08:00
|
|
|
|
BOOL shouldClear = NO;
|
|
|
|
|
|
if (self.showClearLabelEnabled) {
|
|
|
|
|
|
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];
|
2025-12-19 18:45:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-26 11:47:44 +08:00
|
|
|
|
#if DEBUG
|
|
|
|
|
|
NSLog(@"[kb_handleBackspaceLongPressEnded] shouldClear=%@ highlighted=%@ labelHidden=%@",
|
|
|
|
|
|
shouldClear ? @"YES" : @"NO",
|
|
|
|
|
|
self.backspaceClearHighlighted ? @"YES" : @"NO",
|
|
|
|
|
|
self.backspaceClearLabel.hidden ? @"YES" : @"NO");
|
|
|
|
|
|
#endif
|
2025-12-19 18:45:14 +08:00
|
|
|
|
self.backspaceHoldActive = NO;
|
|
|
|
|
|
self.backspaceChunkModeActive = NO;
|
|
|
|
|
|
self.backspaceHoldToken += 1;
|
|
|
|
|
|
self.backspaceHasLastTouchPoint = NO;
|
|
|
|
|
|
[self kb_hideBackspaceClearLabel];
|
|
|
|
|
|
if (shouldClear) {
|
|
|
|
|
|
[self kb_clearAllInput];
|
2025-12-23 18:05:01 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
self.pendingClearBefore = nil;
|
|
|
|
|
|
self.pendingClearAfter = nil;
|
2025-12-26 11:47:44 +08:00
|
|
|
|
[[KBInputBufferManager shared] clearPendingClearSnapshot];
|
|
|
|
|
|
[[KBInputBufferManager shared] commitLiveToManual];
|
2025-12-19 18:45:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#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];
|
2025-12-26 14:19:39 +08:00
|
|
|
|
label.text = KBLocalized(@"Clear");
|
2025-12-19 18:45:14 +08:00
|
|
|
|
label.textAlignment = NSTextAlignmentCenter;
|
2025-12-26 14:19:39 +08:00
|
|
|
|
label.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold];
|
2025-12-19 18:45:14 +08:00
|
|
|
|
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 {
|
2025-12-19 19:21:08 +08:00
|
|
|
|
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
|
|
|
|
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
|
|
|
|
|
if (ivc) {
|
2025-12-26 11:47:44 +08:00
|
|
|
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
|
|
|
|
|
[[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy];
|
2025-12-19 19:21:08 +08:00
|
|
|
|
}
|
2025-12-23 18:05:01 +08:00
|
|
|
|
self.pendingClearBefore = nil;
|
|
|
|
|
|
self.pendingClearAfter = nil;
|
2025-12-26 11:47:44 +08:00
|
|
|
|
[[KBInputBufferManager shared] clearPendingClearSnapshot];
|
2025-12-19 18:45:14 +08:00
|
|
|
|
self.backspaceClearToken += 1;
|
2025-12-26 11:47:44 +08:00
|
|
|
|
self.backspaceClearPhase = KBClearPhaseSkipWhitespace;
|
2025-12-19 18:45:14 +08:00
|
|
|
|
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;
|
|
|
|
|
|
NSInteger nextEmptyRounds = emptyRounds;
|
2025-12-26 13:55:07 +08:00
|
|
|
|
static NSCharacterSet *stopBoundarySet = nil;
|
|
|
|
|
|
static NSCharacterSet *trailingBoundarySet = nil;
|
|
|
|
|
|
static NSCharacterSet *trailingWhitespaceSet = nil;
|
2025-12-26 11:47:44 +08:00
|
|
|
|
static dispatch_once_t onceToken;
|
|
|
|
|
|
dispatch_once(&onceToken, ^{
|
2025-12-26 13:55:07 +08:00
|
|
|
|
// stopBoundary: 遇到这些符号就停(不删除它)
|
|
|
|
|
|
// - 句末符号:. ! ? 。!?
|
|
|
|
|
|
// - 省略号:…(中文里“……”常用作句/段落的停顿)
|
|
|
|
|
|
// - 换行:\n \r(段落边界,避免一次“清空”跨段把全文删完)
|
|
|
|
|
|
stopBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?。!?…\n\r\u2028\u2029"];
|
|
|
|
|
|
|
|
|
|
|
|
// trailingBoundary: 允许作为“尾部句末符号”先删掉,再继续删上一句(更接近微信体验)
|
|
|
|
|
|
// 注意:不要把换行/省略号放进来,否则可能跨段/跨停顿继续删。
|
|
|
|
|
|
trailingBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?。!?"];
|
|
|
|
|
|
|
|
|
|
|
|
// trailingWhitespace: 只跳过空格/Tab(不包含换行,换行由 stopBoundarySet 处理)
|
|
|
|
|
|
trailingWhitespaceSet = [NSCharacterSet whitespaceCharacterSet];
|
2025-12-26 11:47:44 +08:00
|
|
|
|
});
|
|
|
|
|
|
KBClearPhase phase = self.backspaceClearPhase;
|
|
|
|
|
|
|
|
|
|
|
|
NSInteger deletedThisTick = 0;
|
|
|
|
|
|
BOOL shouldStop = NO;
|
|
|
|
|
|
NSString *lastBefore = nil;
|
|
|
|
|
|
for (NSInteger i = 0; i < kKBBackspaceClearDeletesPerTick; i++) {
|
|
|
|
|
|
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
|
|
|
|
|
if (before.length == 0) {
|
|
|
|
|
|
nextEmptyRounds += 1;
|
2025-12-26 13:55:07 +08:00
|
|
|
|
// 宿主(微信/QQ 等)可能在长文本场景下返回空 context,即使还有很多内容。
|
|
|
|
|
|
// 为了避免一次“清空”误删全文:一旦拿不到 before,就立刻停止本次清空。
|
|
|
|
|
|
shouldStop = YES;
|
2025-12-26 11:47:44 +08:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
2025-12-19 18:45:14 +08:00
|
|
|
|
nextEmptyRounds = 0;
|
|
|
|
|
|
|
2025-12-26 11:47:44 +08:00
|
|
|
|
if (lastBefore && [before isEqualToString:lastBefore] && deletedThisTick > 0) {
|
|
|
|
|
|
// 宿主未及时刷新 context,留到下一 tick 再继续,避免越界/重复记录
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
lastBefore = before;
|
|
|
|
|
|
|
|
|
|
|
|
// 取最后一个组合字符
|
|
|
|
|
|
__block NSString *lastChar = @"";
|
|
|
|
|
|
[before enumerateSubstringsInRange:NSMakeRange(0, before.length)
|
|
|
|
|
|
options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse
|
|
|
|
|
|
usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) {
|
|
|
|
|
|
lastChar = substring ?: @"";
|
|
|
|
|
|
*stop = YES;
|
|
|
|
|
|
}];
|
|
|
|
|
|
if (lastChar.length == 0) { break; }
|
2025-12-19 18:45:14 +08:00
|
|
|
|
|
2025-12-26 13:55:07 +08:00
|
|
|
|
BOOL isWhitespace = ([lastChar rangeOfCharacterFromSet:trailingWhitespaceSet].location != NSNotFound);
|
|
|
|
|
|
BOOL isStopBoundary = ([lastChar rangeOfCharacterFromSet:stopBoundarySet].location != NSNotFound);
|
|
|
|
|
|
BOOL isTrailingBoundary = ([lastChar rangeOfCharacterFromSet:trailingBoundarySet].location != NSNotFound);
|
2025-12-19 18:45:14 +08:00
|
|
|
|
|
2025-12-26 11:47:44 +08:00
|
|
|
|
if (phase == KBClearPhaseSkipWhitespace) {
|
|
|
|
|
|
if (isWhitespace) {
|
|
|
|
|
|
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1];
|
|
|
|
|
|
[[KBInputBufferManager shared] applyClearDeleteCount:1];
|
|
|
|
|
|
deletedThisTick += 1;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
phase = KBClearPhaseSkipTrailingBoundary;
|
2025-12-19 18:45:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 11:47:44 +08:00
|
|
|
|
if (phase == KBClearPhaseSkipTrailingBoundary) {
|
2025-12-26 13:55:07 +08:00
|
|
|
|
if (isTrailingBoundary) {
|
2025-12-26 11:47:44 +08:00
|
|
|
|
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1];
|
|
|
|
|
|
[[KBInputBufferManager shared] applyClearDeleteCount:1];
|
|
|
|
|
|
deletedThisTick += 1;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
phase = KBClearPhaseDeleteUntilBoundary;
|
|
|
|
|
|
}
|
2025-12-19 18:45:14 +08:00
|
|
|
|
|
2025-12-26 11:47:44 +08:00
|
|
|
|
// phase == DeleteUntilBoundary
|
2025-12-26 13:55:07 +08:00
|
|
|
|
if (isStopBoundary) {
|
2025-12-26 11:47:44 +08:00
|
|
|
|
shouldStop = YES; // 保留该句末符号
|
|
|
|
|
|
break;
|
2025-12-19 18:45:14 +08:00
|
|
|
|
}
|
2025-12-26 11:47:44 +08:00
|
|
|
|
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1];
|
|
|
|
|
|
[[KBInputBufferManager shared] applyClearDeleteCount:1];
|
|
|
|
|
|
deletedThisTick += 1;
|
|
|
|
|
|
if (guard + deletedThisTick >= kKBBackspaceClearMaxDeletes) { break; }
|
|
|
|
|
|
if (deletedThisTick >= kKBBackspaceClearMaxStep) { break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.backspaceClearPhase = phase;
|
|
|
|
|
|
NSInteger nextGuard = guard + deletedThisTick;
|
|
|
|
|
|
if (nextGuard >= kKBBackspaceClearMaxDeletes ||
|
|
|
|
|
|
nextEmptyRounds > kKBBackspaceClearEmptyContextMaxRounds ||
|
|
|
|
|
|
shouldStop) {
|
|
|
|
|
|
return;
|
2025-12-19 18:45:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
__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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 18:05:01 +08:00
|
|
|
|
- (void)kb_captureDeletionSnapshotIfNeeded {
|
|
|
|
|
|
if ([KBBackspaceUndoManager shared].hasUndo) { return; }
|
|
|
|
|
|
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
|
|
|
|
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
|
|
|
|
|
if (!ivc) { return; }
|
|
|
|
|
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
|
|
|
|
|
[[KBBackspaceUndoManager shared] recordDeletionSnapshotBefore:proxy.documentContextBeforeInput
|
|
|
|
|
|
after:proxy.documentContextAfterInput];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)kb_capturePendingClearSnapshotIfNeeded {
|
|
|
|
|
|
if (self.pendingClearBefore.length > 0 || self.pendingClearAfter.length > 0) { return; }
|
|
|
|
|
|
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
|
|
|
|
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
|
|
|
|
|
if (!ivc) { return; }
|
|
|
|
|
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
|
|
|
|
|
self.pendingClearBefore = proxy.documentContextBeforeInput ?: @"";
|
|
|
|
|
|
self.pendingClearAfter = proxy.documentContextAfterInput ?: @"";
|
2025-12-26 11:47:44 +08:00
|
|
|
|
#if DEBUG
|
|
|
|
|
|
NSLog(@"[kb_capturePendingClearSnapshotIfNeeded/before] len=%lu text=%@", (unsigned long)self.pendingClearBefore.length, self.pendingClearBefore);
|
|
|
|
|
|
NSLog(@"[kb_capturePendingClearSnapshotIfNeeded/after] len=%lu text=%@", (unsigned long)self.pendingClearAfter.length, self.pendingClearAfter);
|
|
|
|
|
|
#endif
|
2025-12-23 18:05:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-19 18:45:14 +08:00
|
|
|
|
@end
|