处理键盘长按删除 撤销出现的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
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapKey:(KBKey *)key {
if (key.type != KBKeyTypeShift && key.type != KBKeyTypeModeChange) {
[[KBBackspaceUndoManager shared] registerNonClearAction];
}
switch (key.type) {
case KBKeyTypeCharacter: {
[[KBBackspaceUndoManager shared] registerNonClearAction];
NSString *text = key.output ?: key.title ?: @"";
[self.textDocumentProxy insertText:text];
[self kb_updateCurrentWordWithInsertedText:text];
} break;
case KBKeyTypeBackspace:
[[KBBackspaceUndoManager shared] recordDeletionSnapshotBefore:self.textDocumentProxy.documentContextBeforeInput
after:self.textDocumentProxy.documentContextAfterInput];
[self.textDocumentProxy deleteBackward];
[self kb_scheduleContextRefreshResetSuppression:NO];
break;
case KBKeyTypeSpace:
[[KBBackspaceUndoManager shared] registerNonClearAction];
[self.textDocumentProxy insertText:@" "];
[self kb_clearCurrentWord];
break;
case KBKeyTypeReturn:
[[KBBackspaceUndoManager shared] registerNonClearAction];
[self.textDocumentProxy insertText:@"\n"];
[self kb_clearCurrentWord];
break;
case KBKeyTypeGlobe:
[self advanceToNextInputMode]; break;
case KBKeyTypeCustom:
[[KBBackspaceUndoManager shared] registerNonClearAction];
//
[self showFunctionPanel:YES];
[self kb_clearCurrentWord];

View File

@@ -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

View File

@@ -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

View File

@@ -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; }
if (!self.lastActionWasClear) {
[self.segments removeAllObjects];
- (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;
}
[self.segments addObject:segment];
self.lastActionWasClear = YES;
- (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;
}
}
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;
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];
}
}
NSInteger searchEnd = end;
while (searchEnd > 0) {
unichar ch = [context characterAtIndex:searchEnd - 1];
if ([sentenceBoundarySet characterIsMember:ch]) {
searchEnd -= 1;
} else {
break;
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

View File

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

View File

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