diff --git a/CustomKeyboard/KeyboardViewController.m b/CustomKeyboard/KeyboardViewController.m index 7e6d2c4..1be1c88 100644 --- a/CustomKeyboard/KeyboardViewController.m +++ b/CustomKeyboard/KeyboardViewController.m @@ -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]; diff --git a/CustomKeyboard/Utils/KBBackspaceLongPressHandler.m b/CustomKeyboard/Utils/KBBackspaceLongPressHandler.m index ac3e3cd..0a34511 100644 --- a/CustomKeyboard/Utils/KBBackspaceLongPressHandler.m +++ b/CustomKeyboard/Utils/KBBackspaceLongPressHandler.m @@ -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 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 proxy = ivc.textDocumentProxy; + self.pendingClearBefore = proxy.documentContextBeforeInput ?: @""; + self.pendingClearAfter = proxy.documentContextAfterInput ?: @""; +} + @end diff --git a/CustomKeyboard/Utils/KBBackspaceUndoManager.h b/CustomKeyboard/Utils/KBBackspaceUndoManager.h index 9b11946..573dd4d 100644 --- a/CustomKeyboard/Utils/KBBackspaceUndoManager.h +++ b/CustomKeyboard/Utils/KBBackspaceUndoManager.h @@ -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 diff --git a/CustomKeyboard/Utils/KBBackspaceUndoManager.m b/CustomKeyboard/Utils/KBBackspaceUndoManager.m index f6144a4..7cc4e3c 100644 --- a/CustomKeyboard/Utils/KBBackspaceUndoManager.m +++ b/CustomKeyboard/Utils/KBBackspaceUndoManager.m @@ -9,8 +9,8 @@ NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspaceUndoStateDidChangeNotification"; @interface KBBackspaceUndoManager () -@property (nonatomic, strong) NSMutableArray *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 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)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 *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 diff --git a/CustomKeyboard/View/KBFunctionView.m b/CustomKeyboard/View/KBFunctionView.m index 6d74774..b058db0 100644 --- a/CustomKeyboard/View/KBFunctionView.m +++ b/CustomKeyboard/View/KBFunctionView.m @@ -770,18 +770,21 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C - (void)kb_fullAccessChanged { dispatch_async(dispatch_get_main_queue(), ^{ [self kb_refreshPasteboardMonitor]; }); } - - (void)onTapDelete { + +- (void)onTapDelete { NSLog(@"点击:删除"); - [[KBBackspaceUndoManager shared] registerNonClearAction]; UIInputViewController *ivc = KBFindInputViewController(self); id proxy = ivc.textDocumentProxy; + [[KBBackspaceUndoManager shared] recordDeletionSnapshotBefore:proxy.documentContextBeforeInput + after:proxy.documentContextAfterInput]; [proxy deleteBackward]; } - (void)onTapClear { NSLog(@"点击:清空"); [self.backspaceHandler performClearAction]; } - - (void)onTapSend { + +- (void)onTapSend { NSLog(@"点击:发送"); [[KBBackspaceUndoManager shared] registerNonClearAction]; // 发送:插入换行。大多数聊天类 App 会把回车视为“发送” diff --git a/CustomKeyboard/View/KBKeyBoardMainView.m b/CustomKeyboard/View/KBKeyBoardMainView.m index a122dec..5a1528b 100644 --- a/CustomKeyboard/View/KBKeyBoardMainView.m +++ b/CustomKeyboard/View/KBKeyBoardMainView.m @@ -14,6 +14,7 @@ #import "KBSuggestionBarView.h" #import "Masonry.h" #import "KBSkinManager.h" +#import "KBBackspaceUndoManager.h" @interface KBKeyBoardMainView () @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 *)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