处理键盘长按删除 撤销出现的bug
This commit is contained in:
@@ -48,6 +48,8 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
@property (nonatomic, assign) CGPoint backspaceLastTouchPointInSelf;
|
||||
@property (nonatomic, assign) NSUInteger backspaceClearToken;
|
||||
@property (nonatomic, strong) UILabel *backspaceClearLabel;
|
||||
@property (nonatomic, copy) NSString *pendingClearBefore;
|
||||
@property (nonatomic, copy) NSString *pendingClearAfter;
|
||||
@end
|
||||
|
||||
@implementation KBBackspaceLongPressHandler
|
||||
@@ -73,6 +75,8 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
self.backspaceHasLastTouchPoint = NO;
|
||||
self.backspaceHoldToken += 1;
|
||||
[self kb_hideBackspaceClearLabel];
|
||||
self.pendingClearBefore = nil;
|
||||
self.pendingClearAfter = nil;
|
||||
|
||||
if (!button) { return; }
|
||||
|
||||
@@ -99,7 +103,10 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
}
|
||||
switch (gr.state) {
|
||||
case UIGestureRecognizerStateBegan: {
|
||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||
[self kb_captureDeletionSnapshotIfNeeded];
|
||||
if (self.showClearLabelEnabled) {
|
||||
[self kb_capturePendingClearSnapshotIfNeeded];
|
||||
}
|
||||
self.backspaceHoldToken += 1;
|
||||
NSUInteger token = self.backspaceHoldToken;
|
||||
self.backspaceHoldActive = YES;
|
||||
@@ -310,6 +317,9 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
[self kb_hideBackspaceClearLabel];
|
||||
if (shouldClear) {
|
||||
[self kb_clearAllInput];
|
||||
} else {
|
||||
self.pendingClearBefore = nil;
|
||||
self.pendingClearAfter = nil;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,9 +431,12 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||
if (ivc) {
|
||||
NSString *before = ivc.textDocumentProxy.documentContextBeforeInput ?: @"";
|
||||
[[KBBackspaceUndoManager shared] recordClearWithContext:before];
|
||||
NSString *before = self.pendingClearBefore ?: (ivc.textDocumentProxy.documentContextBeforeInput ?: @"");
|
||||
NSString *after = self.pendingClearAfter ?: (ivc.textDocumentProxy.documentContextAfterInput ?: @"");
|
||||
[[KBBackspaceUndoManager shared] recordClearWithContextBefore:before after:after];
|
||||
}
|
||||
self.pendingClearBefore = nil;
|
||||
self.pendingClearAfter = nil;
|
||||
self.backspaceClearToken += 1;
|
||||
NSUInteger token = self.backspaceClearToken;
|
||||
[self kb_clearAllInputStepForToken:token guard:0 emptyRounds:0];
|
||||
@@ -489,4 +502,25 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
return self.backspaceButton.superview;
|
||||
}
|
||||
|
||||
- (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 ([KBBackspaceUndoManager shared].hasUndo) { return; }
|
||||
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 ?: @"";
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -15,13 +15,16 @@ extern NSNotificationName const KBBackspaceUndoStateDidChangeNotification;
|
||||
|
||||
+ (instancetype)shared;
|
||||
|
||||
/// 记录一次“立刻清空”删除的内容(基于 documentContextBeforeInput)
|
||||
- (void)recordClearWithContext:(NSString *)context;
|
||||
/// 记录一次删除前的快照(不改变撤销按钮显示)。
|
||||
- (void)recordDeletionSnapshotBefore:(NSString *)before after:(NSString *)after;
|
||||
|
||||
/// 记录一次“立刻清空”删除的内容(基于 documentContextBeforeInput/AfterInput)。
|
||||
- (void)recordClearWithContextBefore:(NSString *)before after:(NSString *)after;
|
||||
|
||||
/// 在指定 responder 处执行撤销(向光标处插回删除的内容)
|
||||
- (void)performUndoFromResponder:(UIResponder *)responder;
|
||||
|
||||
/// 非清空行为触发时,清理撤销状态
|
||||
/// 非删除行为触发时,清理撤销状态
|
||||
- (void)registerNonClearAction;
|
||||
|
||||
@end
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspaceUndoStateDidChangeNotification";
|
||||
|
||||
@interface KBBackspaceUndoManager ()
|
||||
@property (nonatomic, strong) NSMutableArray<NSString *> *segments; // deletion order (last -> first)
|
||||
@property (nonatomic, assign) BOOL lastActionWasClear;
|
||||
@property (nonatomic, copy) NSString *undoText;
|
||||
@property (nonatomic, assign) NSInteger undoAfterLength;
|
||||
@property (nonatomic, assign) BOOL hasUndo;
|
||||
@end
|
||||
|
||||
@@ -27,42 +27,57 @@ NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspa
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
_segments = [NSMutableArray array];
|
||||
_undoText = @"";
|
||||
_undoAfterLength = 0;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)recordClearWithContext:(NSString *)context {
|
||||
if (context.length == 0) { return; }
|
||||
NSString *segment = [self kb_segmentForClearFromContext:context];
|
||||
if (segment.length == 0) { return; }
|
||||
- (void)recordDeletionSnapshotBefore:(NSString *)before after:(NSString *)after {
|
||||
if (self.undoText.length > 0) { return; }
|
||||
NSString *safeBefore = before ?: @"";
|
||||
NSString *safeAfter = after ?: @"";
|
||||
NSString *full = [safeBefore stringByAppendingString:safeAfter];
|
||||
if (full.length == 0) { return; }
|
||||
self.undoText = full;
|
||||
self.undoAfterLength = (NSInteger)safeAfter.length;
|
||||
}
|
||||
|
||||
if (!self.lastActionWasClear) {
|
||||
[self.segments removeAllObjects];
|
||||
- (void)recordClearWithContextBefore:(NSString *)before after:(NSString *)after {
|
||||
if (self.undoText.length == 0) {
|
||||
NSString *safeBefore = before ?: @"";
|
||||
NSString *safeAfter = after ?: @"";
|
||||
NSString *full = [safeBefore stringByAppendingString:safeAfter];
|
||||
if (full.length > 0) {
|
||||
self.undoText = full;
|
||||
self.undoAfterLength = (NSInteger)safeAfter.length;
|
||||
}
|
||||
}
|
||||
[self.segments addObject:segment];
|
||||
self.lastActionWasClear = YES;
|
||||
if (self.undoText.length == 0) { return; }
|
||||
[self kb_updateHasUndo:YES];
|
||||
}
|
||||
|
||||
- (void)performUndoFromResponder:(UIResponder *)responder {
|
||||
if (self.segments.count == 0) { return; }
|
||||
if (self.undoText.length == 0) { return; }
|
||||
UIInputViewController *ivc = KBFindInputViewController(responder);
|
||||
if (!ivc) { return; }
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
NSString *text = [self kb_buildUndoText];
|
||||
if (text.length == 0) { return; }
|
||||
[proxy insertText:text];
|
||||
[self kb_clearAllTextForProxy:proxy];
|
||||
[proxy insertText:self.undoText];
|
||||
if (self.undoAfterLength > 0 &&
|
||||
[proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) {
|
||||
[proxy adjustTextPositionByCharacterOffset:-self.undoAfterLength];
|
||||
}
|
||||
|
||||
[self.segments removeAllObjects];
|
||||
self.lastActionWasClear = NO;
|
||||
self.undoText = @"";
|
||||
self.undoAfterLength = 0;
|
||||
[self kb_updateHasUndo:NO];
|
||||
}
|
||||
|
||||
- (void)registerNonClearAction {
|
||||
self.lastActionWasClear = NO;
|
||||
if (self.segments.count == 0) { return; }
|
||||
[self.segments removeAllObjects];
|
||||
if (self.undoText.length == 0) { return; }
|
||||
self.undoText = @"";
|
||||
self.undoAfterLength = 0;
|
||||
[self kb_updateHasUndo:NO];
|
||||
}
|
||||
|
||||
@@ -74,97 +89,34 @@ NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspa
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBBackspaceUndoStateDidChangeNotification object:self];
|
||||
}
|
||||
|
||||
- (NSString *)kb_segmentForClearFromContext:(NSString *)context {
|
||||
NSInteger length = context.length;
|
||||
if (length == 0) { return @""; }
|
||||
static const NSInteger kKBUndoClearMaxRounds = 200;
|
||||
|
||||
static NSCharacterSet *sentenceBoundarySet = nil;
|
||||
static NSCharacterSet *whitespaceSet = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;。!?;…\n"];
|
||||
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||||
});
|
||||
- (void)kb_clearAllTextForProxy:(id<UITextDocumentProxy>)proxy {
|
||||
if (!proxy) { return; }
|
||||
|
||||
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;
|
||||
if ([proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) {
|
||||
NSInteger guard = 0;
|
||||
NSString *contextAfter = proxy.documentContextAfterInput ?: @"";
|
||||
while (contextAfter.length > 0 && guard < kKBUndoClearMaxRounds) {
|
||||
NSInteger offset = (NSInteger)contextAfter.length;
|
||||
[proxy adjustTextPositionByCharacterOffset:offset];
|
||||
for (NSUInteger i = 0; i < contextAfter.length; i++) {
|
||||
[proxy deleteBackward];
|
||||
}
|
||||
guard += 1;
|
||||
contextAfter = proxy.documentContextAfterInput ?: @"";
|
||||
}
|
||||
}
|
||||
|
||||
NSInteger boundaryIndex = NSNotFound;
|
||||
for (NSInteger i = searchEnd - 1; i >= 0; i--) {
|
||||
unichar ch = [context characterAtIndex:i];
|
||||
if ([sentenceBoundarySet characterIsMember:ch]) {
|
||||
boundaryIndex = i;
|
||||
break;
|
||||
NSInteger guard = 0;
|
||||
NSString *contextBefore = proxy.documentContextBeforeInput ?: @"";
|
||||
while (contextBefore.length > 0 && guard < kKBUndoClearMaxRounds) {
|
||||
for (NSUInteger i = 0; i < contextBefore.length; i++) {
|
||||
[proxy deleteBackward];
|
||||
}
|
||||
guard += 1;
|
||||
contextBefore = proxy.documentContextBeforeInput ?: @"";
|
||||
}
|
||||
|
||||
NSInteger start = (boundaryIndex == NSNotFound) ? 0 : (boundaryIndex + 1);
|
||||
if (start >= length) { return @""; }
|
||||
return [context substringFromIndex:start];
|
||||
}
|
||||
|
||||
- (NSString *)kb_buildUndoText {
|
||||
if (self.segments.count == 0) { return @""; }
|
||||
NSArray<NSString *> *ordered = [[self.segments reverseObjectEnumerator] allObjects];
|
||||
NSMutableString *result = [NSMutableString string];
|
||||
for (NSInteger i = 0; i < ordered.count; i++) {
|
||||
NSString *segment = ordered[i] ?: @"";
|
||||
if (segment.length == 0) { continue; }
|
||||
if (i < ordered.count - 1) {
|
||||
segment = [self kb_replaceTrailingBoundaryWithComma:segment];
|
||||
}
|
||||
[result appendString:segment];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSString *)kb_replaceTrailingBoundaryWithComma:(NSString *)segment {
|
||||
if (segment.length == 0) { return segment; }
|
||||
|
||||
static NSCharacterSet *boundarySet = nil;
|
||||
static NSCharacterSet *englishBoundarySet = nil;
|
||||
static NSCharacterSet *whitespaceSet = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
boundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;。!?;…\n"];
|
||||
englishBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;"];
|
||||
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||||
});
|
||||
|
||||
NSInteger idx = segment.length - 1;
|
||||
while (idx >= 0) {
|
||||
unichar ch = [segment characterAtIndex:idx];
|
||||
if ([whitespaceSet characterIsMember:ch]) {
|
||||
idx -= 1;
|
||||
continue;
|
||||
}
|
||||
if (![boundarySet characterIsMember:ch]) {
|
||||
return segment;
|
||||
}
|
||||
NSString *comma = [englishBoundarySet characterIsMember:ch] ? @"," : @",";
|
||||
NSMutableString *mutable = [segment mutableCopy];
|
||||
NSRange r = NSMakeRange(idx, 1);
|
||||
[mutable replaceCharactersInRange:r withString:comma];
|
||||
return mutable;
|
||||
}
|
||||
|
||||
return segment;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
Reference in New Issue
Block a user