处理键盘长按删除 撤销出现的bug

This commit is contained in:
2025-12-23 18:05:01 +08:00
parent 73d6ec933a
commit 6a539dc3c5
6 changed files with 141 additions and 126 deletions

View File

@@ -363,30 +363,33 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
// MARK: - KBKeyBoardMainViewDelegate // MARK: - KBKeyBoardMainViewDelegate
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapKey:(KBKey *)key { - (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapKey:(KBKey *)key {
if (key.type != KBKeyTypeShift && key.type != KBKeyTypeModeChange) {
[[KBBackspaceUndoManager shared] registerNonClearAction];
}
switch (key.type) { switch (key.type) {
case KBKeyTypeCharacter: { case KBKeyTypeCharacter: {
[[KBBackspaceUndoManager shared] registerNonClearAction];
NSString *text = key.output ?: key.title ?: @""; NSString *text = key.output ?: key.title ?: @"";
[self.textDocumentProxy insertText:text]; [self.textDocumentProxy insertText:text];
[self kb_updateCurrentWordWithInsertedText:text]; [self kb_updateCurrentWordWithInsertedText:text];
} break; } break;
case KBKeyTypeBackspace: case KBKeyTypeBackspace:
[[KBBackspaceUndoManager shared] recordDeletionSnapshotBefore:self.textDocumentProxy.documentContextBeforeInput
after:self.textDocumentProxy.documentContextAfterInput];
[self.textDocumentProxy deleteBackward]; [self.textDocumentProxy deleteBackward];
[self kb_scheduleContextRefreshResetSuppression:NO]; [self kb_scheduleContextRefreshResetSuppression:NO];
break; break;
case KBKeyTypeSpace: case KBKeyTypeSpace:
[[KBBackspaceUndoManager shared] registerNonClearAction];
[self.textDocumentProxy insertText:@" "]; [self.textDocumentProxy insertText:@" "];
[self kb_clearCurrentWord]; [self kb_clearCurrentWord];
break; break;
case KBKeyTypeReturn: case KBKeyTypeReturn:
[[KBBackspaceUndoManager shared] registerNonClearAction];
[self.textDocumentProxy insertText:@"\n"]; [self.textDocumentProxy insertText:@"\n"];
[self kb_clearCurrentWord]; [self kb_clearCurrentWord];
break; break;
case KBKeyTypeGlobe: case KBKeyTypeGlobe:
[self advanceToNextInputMode]; break; [self advanceToNextInputMode]; break;
case KBKeyTypeCustom: case KBKeyTypeCustom:
[[KBBackspaceUndoManager shared] registerNonClearAction];
// //
[self showFunctionPanel:YES]; [self showFunctionPanel:YES];
[self kb_clearCurrentWord]; [self kb_clearCurrentWord];

View File

@@ -48,6 +48,8 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
@property (nonatomic, assign) CGPoint backspaceLastTouchPointInSelf; @property (nonatomic, assign) CGPoint backspaceLastTouchPointInSelf;
@property (nonatomic, assign) NSUInteger backspaceClearToken; @property (nonatomic, assign) NSUInteger backspaceClearToken;
@property (nonatomic, strong) UILabel *backspaceClearLabel; @property (nonatomic, strong) UILabel *backspaceClearLabel;
@property (nonatomic, copy) NSString *pendingClearBefore;
@property (nonatomic, copy) NSString *pendingClearAfter;
@end @end
@implementation KBBackspaceLongPressHandler @implementation KBBackspaceLongPressHandler
@@ -73,6 +75,8 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
self.backspaceHasLastTouchPoint = NO; self.backspaceHasLastTouchPoint = NO;
self.backspaceHoldToken += 1; self.backspaceHoldToken += 1;
[self kb_hideBackspaceClearLabel]; [self kb_hideBackspaceClearLabel];
self.pendingClearBefore = nil;
self.pendingClearAfter = nil;
if (!button) { return; } if (!button) { return; }
@@ -99,7 +103,10 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
} }
switch (gr.state) { switch (gr.state) {
case UIGestureRecognizerStateBegan: { case UIGestureRecognizerStateBegan: {
[[KBBackspaceUndoManager shared] registerNonClearAction]; [self kb_captureDeletionSnapshotIfNeeded];
if (self.showClearLabelEnabled) {
[self kb_capturePendingClearSnapshotIfNeeded];
}
self.backspaceHoldToken += 1; self.backspaceHoldToken += 1;
NSUInteger token = self.backspaceHoldToken; NSUInteger token = self.backspaceHoldToken;
self.backspaceHoldActive = YES; self.backspaceHoldActive = YES;
@@ -310,6 +317,9 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
[self kb_hideBackspaceClearLabel]; [self kb_hideBackspaceClearLabel];
if (shouldClear) { if (shouldClear) {
[self kb_clearAllInput]; [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); UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
UIInputViewController *ivc = KBFindInputViewController(start); UIInputViewController *ivc = KBFindInputViewController(start);
if (ivc) { if (ivc) {
NSString *before = ivc.textDocumentProxy.documentContextBeforeInput ?: @""; NSString *before = self.pendingClearBefore ?: (ivc.textDocumentProxy.documentContextBeforeInput ?: @"");
[[KBBackspaceUndoManager shared] recordClearWithContext:before]; NSString *after = self.pendingClearAfter ?: (ivc.textDocumentProxy.documentContextAfterInput ?: @"");
[[KBBackspaceUndoManager shared] recordClearWithContextBefore:before after:after];
} }
self.pendingClearBefore = nil;
self.pendingClearAfter = nil;
self.backspaceClearToken += 1; self.backspaceClearToken += 1;
NSUInteger token = self.backspaceClearToken; NSUInteger token = self.backspaceClearToken;
[self kb_clearAllInputStepForToken:token guard:0 emptyRounds:0]; [self kb_clearAllInputStepForToken:token guard:0 emptyRounds:0];
@@ -489,4 +502,25 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
return self.backspaceButton.superview; 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 @end

View File

@@ -15,13 +15,16 @@ extern NSNotificationName const KBBackspaceUndoStateDidChangeNotification;
+ (instancetype)shared; + (instancetype)shared;
/// 记录一次“立刻清空”删除的内容(基于 documentContextBeforeInput /// 记录一次删除前的快照(不改变撤销按钮显示)。
- (void)recordClearWithContext:(NSString *)context; - (void)recordDeletionSnapshotBefore:(NSString *)before after:(NSString *)after;
/// 记录一次“立刻清空”删除的内容(基于 documentContextBeforeInput/AfterInput
- (void)recordClearWithContextBefore:(NSString *)before after:(NSString *)after;
/// 在指定 responder 处执行撤销(向光标处插回删除的内容) /// 在指定 responder 处执行撤销(向光标处插回删除的内容)
- (void)performUndoFromResponder:(UIResponder *)responder; - (void)performUndoFromResponder:(UIResponder *)responder;
/// 非清空行为触发时,清理撤销状态 /// 非删除行为触发时,清理撤销状态
- (void)registerNonClearAction; - (void)registerNonClearAction;
@end @end

View File

@@ -9,8 +9,8 @@
NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspaceUndoStateDidChangeNotification"; NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspaceUndoStateDidChangeNotification";
@interface KBBackspaceUndoManager () @interface KBBackspaceUndoManager ()
@property (nonatomic, strong) NSMutableArray<NSString *> *segments; // deletion order (last -> first) @property (nonatomic, copy) NSString *undoText;
@property (nonatomic, assign) BOOL lastActionWasClear; @property (nonatomic, assign) NSInteger undoAfterLength;
@property (nonatomic, assign) BOOL hasUndo; @property (nonatomic, assign) BOOL hasUndo;
@end @end
@@ -27,42 +27,57 @@ NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspa
- (instancetype)init { - (instancetype)init {
if (self = [super init]) { if (self = [super init]) {
_segments = [NSMutableArray array]; _undoText = @"";
_undoAfterLength = 0;
} }
return self; return self;
} }
- (void)recordClearWithContext:(NSString *)context { - (void)recordDeletionSnapshotBefore:(NSString *)before after:(NSString *)after {
if (context.length == 0) { return; } if (self.undoText.length > 0) { return; }
NSString *segment = [self kb_segmentForClearFromContext:context]; NSString *safeBefore = before ?: @"";
if (segment.length == 0) { return; } NSString *safeAfter = after ?: @"";
NSString *full = [safeBefore stringByAppendingString:safeAfter];
if (full.length == 0) { return; }
self.undoText = full;
self.undoAfterLength = (NSInteger)safeAfter.length;
}
if (!self.lastActionWasClear) { - (void)recordClearWithContextBefore:(NSString *)before after:(NSString *)after {
[self.segments removeAllObjects]; 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]; if (self.undoText.length == 0) { return; }
self.lastActionWasClear = YES;
[self kb_updateHasUndo:YES]; [self kb_updateHasUndo:YES];
} }
- (void)performUndoFromResponder:(UIResponder *)responder { - (void)performUndoFromResponder:(UIResponder *)responder {
if (self.segments.count == 0) { return; } if (self.undoText.length == 0) { return; }
UIInputViewController *ivc = KBFindInputViewController(responder); UIInputViewController *ivc = KBFindInputViewController(responder);
if (!ivc) { return; } if (!ivc) { return; }
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy; id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
NSString *text = [self kb_buildUndoText]; [self kb_clearAllTextForProxy:proxy];
if (text.length == 0) { return; } [proxy insertText:self.undoText];
[proxy insertText:text]; if (self.undoAfterLength > 0 &&
[proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) {
[proxy adjustTextPositionByCharacterOffset:-self.undoAfterLength];
}
[self.segments removeAllObjects]; self.undoText = @"";
self.lastActionWasClear = NO; self.undoAfterLength = 0;
[self kb_updateHasUndo:NO]; [self kb_updateHasUndo:NO];
} }
- (void)registerNonClearAction { - (void)registerNonClearAction {
self.lastActionWasClear = NO; if (self.undoText.length == 0) { return; }
if (self.segments.count == 0) { return; } self.undoText = @"";
[self.segments removeAllObjects]; self.undoAfterLength = 0;
[self kb_updateHasUndo:NO]; [self kb_updateHasUndo:NO];
} }
@@ -74,97 +89,34 @@ NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspa
[[NSNotificationCenter defaultCenter] postNotificationName:KBBackspaceUndoStateDidChangeNotification object:self]; [[NSNotificationCenter defaultCenter] postNotificationName:KBBackspaceUndoStateDidChangeNotification object:self];
} }
- (NSString *)kb_segmentForClearFromContext:(NSString *)context { static const NSInteger kKBUndoClearMaxRounds = 200;
NSInteger length = context.length;
if (length == 0) { return @""; }
static NSCharacterSet *sentenceBoundarySet = nil; - (void)kb_clearAllTextForProxy:(id<UITextDocumentProxy>)proxy {
static NSCharacterSet *whitespaceSet = nil; if (!proxy) { return; }
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;。!?;…\n"];
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
});
NSInteger end = length; if ([proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) {
while (end > 0) { NSInteger guard = 0;
unichar ch = [context characterAtIndex:end - 1]; NSString *contextAfter = proxy.documentContextAfterInput ?: @"";
if ([whitespaceSet characterIsMember:ch]) { while (contextAfter.length > 0 && guard < kKBUndoClearMaxRounds) {
end -= 1; NSInteger offset = (NSInteger)contextAfter.length;
} else { [proxy adjustTextPositionByCharacterOffset:offset];
break; for (NSUInteger i = 0; i < contextAfter.length; i++) {
} [proxy deleteBackward];
} }
NSInteger searchEnd = end; guard += 1;
while (searchEnd > 0) { contextAfter = proxy.documentContextAfterInput ?: @"";
unichar ch = [context characterAtIndex:searchEnd - 1];
if ([sentenceBoundarySet characterIsMember:ch]) {
searchEnd -= 1;
} else {
break;
} }
} }
NSInteger boundaryIndex = NSNotFound; NSInteger guard = 0;
for (NSInteger i = searchEnd - 1; i >= 0; i--) { NSString *contextBefore = proxy.documentContextBeforeInput ?: @"";
unichar ch = [context characterAtIndex:i]; while (contextBefore.length > 0 && guard < kKBUndoClearMaxRounds) {
if ([sentenceBoundarySet characterIsMember:ch]) { for (NSUInteger i = 0; i < contextBefore.length; i++) {
boundaryIndex = i; [proxy deleteBackward];
break;
} }
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 @end

View File

@@ -770,18 +770,21 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C
- (void)kb_fullAccessChanged { - (void)kb_fullAccessChanged {
dispatch_async(dispatch_get_main_queue(), ^{ [self kb_refreshPasteboardMonitor]; }); dispatch_async(dispatch_get_main_queue(), ^{ [self kb_refreshPasteboardMonitor]; });
} }
- (void)onTapDelete {
- (void)onTapDelete {
NSLog(@"点击:删除"); NSLog(@"点击:删除");
[[KBBackspaceUndoManager shared] registerNonClearAction];
UIInputViewController *ivc = KBFindInputViewController(self); UIInputViewController *ivc = KBFindInputViewController(self);
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy; id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
[[KBBackspaceUndoManager shared] recordDeletionSnapshotBefore:proxy.documentContextBeforeInput
after:proxy.documentContextAfterInput];
[proxy deleteBackward]; [proxy deleteBackward];
} }
- (void)onTapClear { - (void)onTapClear {
NSLog(@"点击:清空"); NSLog(@"点击:清空");
[self.backspaceHandler performClearAction]; [self.backspaceHandler performClearAction];
} }
- (void)onTapSend {
- (void)onTapSend {
NSLog(@"点击:发送"); NSLog(@"点击:发送");
[[KBBackspaceUndoManager shared] registerNonClearAction]; [[KBBackspaceUndoManager shared] registerNonClearAction];
// App // App

View File

@@ -14,6 +14,7 @@
#import "KBSuggestionBarView.h" #import "KBSuggestionBarView.h"
#import "Masonry.h" #import "Masonry.h"
#import "KBSkinManager.h" #import "KBSkinManager.h"
#import "KBBackspaceUndoManager.h"
@interface KBKeyBoardMainView ()<KBToolBarDelegate, KBKeyboardViewDelegate, KBEmojiPanelViewDelegate, KBSuggestionBarViewDelegate> @interface KBKeyBoardMainView ()<KBToolBarDelegate, KBKeyboardViewDelegate, KBEmojiPanelViewDelegate, KBSuggestionBarViewDelegate>
@property (nonatomic, strong) KBToolBar *topBar; @property (nonatomic, strong) KBToolBar *topBar;
@@ -87,10 +88,18 @@
make.top.equalTo(self.topBar.mas_bottom).offset(barSpacing); make.top.equalTo(self.topBar.mas_bottom).offset(barSpacing);
}]; }];
// / // /
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(kb_undoStateChanged)
name:KBBackspaceUndoStateDidChangeNotification
object:nil];
} }
return self; return self;
} }
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)setEmojiPanelVisible:(BOOL)visible animated:(BOOL)animated { - (void)setEmojiPanelVisible:(BOOL)visible animated:(BOOL)animated {
if (self.emojiPanelVisible == visible) return; if (self.emojiPanelVisible == visible) return;
self.emojiPanelVisible = visible; self.emojiPanelVisible = visible;
@@ -108,7 +117,7 @@
self.emojiView.alpha = visible ? 1.0 : 0.0; self.emojiView.alpha = visible ? 1.0 : 0.0;
self.keyboardView.alpha = visible ? 0.0 : 1.0; self.keyboardView.alpha = visible ? 0.0 : 1.0;
self.topBar.alpha = visible ? 0.0 : 1.0; self.topBar.alpha = visible ? 0.0 : 1.0;
self.suggestionBar.alpha = visible ? 0.0 : (self.suggestionBarHasItems ? 1.0 : 0.0); self.suggestionBar.alpha = visible ? 0.0 : ([self kb_shouldShowSuggestions] ? 1.0 : 0.0);
}; };
void (^completion)(BOOL) = ^(BOOL finished) { void (^completion)(BOOL) = ^(BOOL finished) {
self.emojiView.hidden = !visible; self.emojiView.hidden = !visible;
@@ -117,7 +126,7 @@
if (visible) { if (visible) {
self.suggestionBar.hidden = YES; self.suggestionBar.hidden = YES;
} else { } else {
self.suggestionBar.hidden = !self.suggestionBarHasItems; self.suggestionBar.hidden = ![self kb_shouldShowSuggestions];
} }
}; };
@@ -258,14 +267,7 @@
- (void)kb_setSuggestions:(NSArray<NSString *> *)suggestions { - (void)kb_setSuggestions:(NSArray<NSString *> *)suggestions {
self.suggestionBarHasItems = (suggestions.count > 0); self.suggestionBarHasItems = (suggestions.count > 0);
[self.suggestionBar updateSuggestions:suggestions]; [self.suggestionBar updateSuggestions:suggestions];
[self kb_applySuggestionVisibility];
if (self.emojiPanelVisible) {
self.suggestionBar.hidden = YES;
self.suggestionBar.alpha = 0.0;
} else {
self.suggestionBar.hidden = !self.suggestionBarHasItems;
self.suggestionBar.alpha = self.suggestionBarHasItems ? 1.0 : 0.0;
}
} }
#pragma mark - KBSuggestionBarViewDelegate #pragma mark - KBSuggestionBarViewDelegate
@@ -276,4 +278,22 @@
} }
} }
- (void)kb_undoStateChanged {
[self kb_applySuggestionVisibility];
}
- (BOOL)kb_shouldShowSuggestions {
if (self.emojiPanelVisible) { return NO; }
if (![KBBackspaceUndoManager shared].hasUndo && self.suggestionBarHasItems) {
return YES;
}
return NO;
}
- (void)kb_applySuggestionVisibility {
BOOL shouldShow = [self kb_shouldShowSuggestions];
self.suggestionBar.hidden = !shouldShow;
self.suggestionBar.alpha = shouldShow ? 1.0 : 0.0;
}
@end @end