From 203f104ece649877b557b4b935a7cd59d48293ca Mon Sep 17 00:00:00 2001 From: CodeST <694468528@qq.com> Date: Fri, 26 Dec 2025 11:47:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=94=AE=E7=9B=98=E9=95=BF?= =?UTF-8?q?=E6=8C=89=E7=AB=8B=E5=8D=B3=E6=B8=85=E7=A9=BA=E5=92=8C=E6=92=A4?= =?UTF-8?q?=E9=94=80=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CustomKeyboard/KeyboardViewController.m | 23 +- .../Utils/KBBackspaceLongPressHandler.h | 4 +- .../Utils/KBBackspaceLongPressHandler.m | 166 ++++++++--- CustomKeyboard/Utils/KBBackspaceUndoManager.h | 3 + CustomKeyboard/Utils/KBBackspaceUndoManager.m | 216 ++++++++++++-- CustomKeyboard/Utils/KBInputBufferManager.h | 34 +++ CustomKeyboard/Utils/KBInputBufferManager.m | 279 ++++++++++++++++++ CustomKeyboard/View/KBFunctionView.m | 12 +- CustomKeyboard/View/KBStreamTextView.m | 2 + .../Logs/Build/LogStoreManifest.plist | 10 + .../Logs/Launch/LogStoreManifest.plist | 10 + .../Logs/Localization/LogStoreManifest.plist | 10 + ...-5595-43EC-B207-003BAB7870CE.xcactivitylog | Bin 0 -> 783 bytes .../Logs/Package/LogStoreManifest.plist | 47 +++ _DerivedData/Logs/Test/LogStoreManifest.plist | 10 + ...DkRM6D7duevJ1pJ9aq75SfwN7YYm12kuCUDnZb4Q== | Bin 0 -> 876 bytes ...rJZ5V7riJQMdl5Wlv_2reSmpPiQiDIqDBW9lQ_YQ== | Bin 0 -> 507 bytes ...-0Nb-iYi6izT5RJ5yA2w_iANvsQaqTEGQDUvjnHA== | Bin 0 -> 633 bytes ...fnDMVRxt2ts61-uq5UX65K4l_LZXCoqTJ2PdnM1w== | Bin 0 -> 193 bytes ...levHrHNamc2oelL32RyN2c9x-M59B2wBAeP3TOAg== | Bin 0 -> 1210 bytes ...DkRM6D7duevJ1pJ9aq75SfwN7YYm12kuCUDnZb4Q== | Bin 0 -> 1 bytes ...rJZ5V7riJQMdl5Wlv_2reSmpPiQiDIqDBW9lQ_YQ== | Bin 0 -> 1 bytes ...-0Nb-iYi6izT5RJ5yA2w_iANvsQaqTEGQDUvjnHA== | Bin 0 -> 67 bytes ...fnDMVRxt2ts61-uq5UX65K4l_LZXCoqTJ2PdnM1w== | Bin 0 -> 1 bytes ...levHrHNamc2oelL32RyN2c9x-M59B2wBAeP3TOAg== | Bin 0 -> 199 bytes _xcodebuild.xcresult/Info.plist | 29 ++ keyBoard.xcodeproj/project.pbxproj | 6 + keyBoard/Class/Pay/M/KBPayProductModel.m | 8 +- 28 files changed, 798 insertions(+), 71 deletions(-) create mode 100644 CustomKeyboard/Utils/KBInputBufferManager.h create mode 100644 CustomKeyboard/Utils/KBInputBufferManager.m create mode 100644 _DerivedData/Logs/Build/LogStoreManifest.plist create mode 100644 _DerivedData/Logs/Launch/LogStoreManifest.plist create mode 100644 _DerivedData/Logs/Localization/LogStoreManifest.plist create mode 100644 _DerivedData/Logs/Package/800731DD-5595-43EC-B207-003BAB7870CE.xcactivitylog create mode 100644 _DerivedData/Logs/Package/LogStoreManifest.plist create mode 100644 _DerivedData/Logs/Test/LogStoreManifest.plist create mode 100644 _xcodebuild.xcresult/Data/data.0~KuxSmpdGJ92pn5u_BEjBWM8AjGpqkfeyzGLIsmXSHlanVADkRM6D7duevJ1pJ9aq75SfwN7YYm12kuCUDnZb4Q== create mode 100644 _xcodebuild.xcresult/Data/data.0~VWXqww6ghRP4ivOuNZRwW_OWaGbj8XBLQxUeJ5gOrIAEQarJZ5V7riJQMdl5Wlv_2reSmpPiQiDIqDBW9lQ_YQ== create mode 100644 _xcodebuild.xcresult/Data/data.0~Vu7KwkETF7p-NPaNF0LJw3Y7WlP-qb7sPA-2rXXX0yfpkv-0Nb-iYi6izT5RJ5yA2w_iANvsQaqTEGQDUvjnHA== create mode 100644 _xcodebuild.xcresult/Data/data.0~vrHCUusPTfhGPOlPQjtQeI9gXBLbXl2rHlLgSQrEbpz8GKfnDMVRxt2ts61-uq5UX65K4l_LZXCoqTJ2PdnM1w== create mode 100644 _xcodebuild.xcresult/Data/data.0~z4eUyi7LNyiJgMc9YvhirRPEbAqQY1U8Utz3Zonm5K5gXqlevHrHNamc2oelL32RyN2c9x-M59B2wBAeP3TOAg== create mode 100644 _xcodebuild.xcresult/Data/refs.0~KuxSmpdGJ92pn5u_BEjBWM8AjGpqkfeyzGLIsmXSHlanVADkRM6D7duevJ1pJ9aq75SfwN7YYm12kuCUDnZb4Q== create mode 100644 _xcodebuild.xcresult/Data/refs.0~VWXqww6ghRP4ivOuNZRwW_OWaGbj8XBLQxUeJ5gOrIAEQarJZ5V7riJQMdl5Wlv_2reSmpPiQiDIqDBW9lQ_YQ== create mode 100644 _xcodebuild.xcresult/Data/refs.0~Vu7KwkETF7p-NPaNF0LJw3Y7WlP-qb7sPA-2rXXX0yfpkv-0Nb-iYi6izT5RJ5yA2w_iANvsQaqTEGQDUvjnHA== create mode 100644 _xcodebuild.xcresult/Data/refs.0~vrHCUusPTfhGPOlPQjtQeI9gXBLbXl2rHlLgSQrEbpz8GKfnDMVRxt2ts61-uq5UX65K4l_LZXCoqTJ2PdnM1w== create mode 100644 _xcodebuild.xcresult/Data/refs.0~z4eUyi7LNyiJgMc9YvhirRPEbAqQY1U8Utz3Zonm5K5gXqlevHrHNamc2oelL32RyN2c9x-M59B2wBAeP3TOAg== create mode 100644 _xcodebuild.xcresult/Info.plist diff --git a/CustomKeyboard/KeyboardViewController.m b/CustomKeyboard/KeyboardViewController.m index 1be1c88..3633946 100644 --- a/CustomKeyboard/KeyboardViewController.m +++ b/CustomKeyboard/KeyboardViewController.m @@ -20,6 +20,7 @@ #import "KBKeyboardSubscriptionView.h" #import "KBKeyboardSubscriptionProduct.h" #import "KBBackspaceUndoManager.h" +#import "KBInputBufferManager.h" #import "KBSuggestionEngine.h" // 提前声明一个类别,使编译器在 static 回调中识别 kb_consumePendingShopSkin 方法。 @@ -93,6 +94,14 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center, - (void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:animated]; [[KBLocalizationManager shared] reloadFromSharedStorageIfNeeded]; + [[KBInputBufferManager shared] seedIfEmptyWithContextBefore:self.textDocumentProxy.documentContextBeforeInput + after:self.textDocumentProxy.documentContextAfterInput]; +} + +- (void)textDidChange:(id)textInput { + [super textDidChange:textInput]; + [[KBInputBufferManager shared] updateFromExternalContextBefore:self.textDocumentProxy.documentContextBeforeInput + after:self.textDocumentProxy.documentContextAfterInput]; } @@ -369,22 +378,27 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center, NSString *text = key.output ?: key.title ?: @""; [self.textDocumentProxy insertText:text]; [self kb_updateCurrentWordWithInsertedText:text]; + [[KBInputBufferManager shared] appendText:text]; } break; case KBKeyTypeBackspace: - [[KBBackspaceUndoManager shared] recordDeletionSnapshotBefore:self.textDocumentProxy.documentContextBeforeInput - after:self.textDocumentProxy.documentContextAfterInput]; - [self.textDocumentProxy deleteBackward]; + [[KBInputBufferManager shared] refreshFromProxyIfPossible:self.textDocumentProxy]; + [[KBInputBufferManager shared] prepareSnapshotForDeleteWithContextBefore:self.textDocumentProxy.documentContextBeforeInput + after:self.textDocumentProxy.documentContextAfterInput]; + [[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:self.textDocumentProxy count:1]; [self kb_scheduleContextRefreshResetSuppression:NO]; + [[KBInputBufferManager shared] applyHoldDeleteCount:1]; break; case KBKeyTypeSpace: [[KBBackspaceUndoManager shared] registerNonClearAction]; [self.textDocumentProxy insertText:@" "]; [self kb_clearCurrentWord]; + [[KBInputBufferManager shared] appendText:@" "]; break; case KBKeyTypeReturn: [[KBBackspaceUndoManager shared] registerNonClearAction]; [self.textDocumentProxy insertText:@"\n"]; [self kb_clearCurrentWord]; + [[KBInputBufferManager shared] appendText:@"\n"]; break; case KBKeyTypeGlobe: [self advanceToNextInputMode]; break; @@ -419,6 +433,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center, [[KBBackspaceUndoManager shared] registerNonClearAction]; [self.textDocumentProxy insertText:emoji]; [self kb_clearCurrentWord]; + [[KBInputBufferManager shared] appendText:emoji]; } - (void)keyBoardMainViewDidTapUndo:(KBKeyBoardMainView *)keyBoardMainView { @@ -432,6 +447,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center, - (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectSuggestion:(NSString *)suggestion { if (suggestion.length == 0) { return; } + [[KBBackspaceUndoManager shared] registerNonClearAction]; NSString *current = self.currentWord ?: @""; if (current.length > 0) { for (NSUInteger i = 0; i < current.length; i++) { @@ -443,6 +459,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center, [self.suggestionEngine recordSelection:suggestion]; self.suppressSuggestions = YES; [self.keyBoardMainView kb_setSuggestions:@[]]; + [[KBInputBufferManager shared] replaceTailWithText:suggestion deleteCount:current.length]; } // MARK: - KBFunctionViewDelegate diff --git a/CustomKeyboard/Utils/KBBackspaceLongPressHandler.h b/CustomKeyboard/Utils/KBBackspaceLongPressHandler.h index 6dd260b..22638e7 100644 --- a/CustomKeyboard/Utils/KBBackspaceLongPressHandler.h +++ b/CustomKeyboard/Utils/KBBackspaceLongPressHandler.h @@ -11,10 +11,10 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithContainerView:(UIView *)containerView; -/// 配置删除按钮(包含长按删除;可选是否显示“立刻清空”提示) +/// 配置删除按钮(包含长按删除;可选是否显示“上滑清空”提示) - (void)bindDeleteButton:(nullable UIView *)button showClearLabel:(BOOL)showClearLabel; -/// 触发“立刻清空”逻辑(可用于功能面板的清空按钮) +/// 触发“上滑清空”逻辑(可用于功能面板的清空按钮) - (void)performClearAction; @end diff --git a/CustomKeyboard/Utils/KBBackspaceLongPressHandler.m b/CustomKeyboard/Utils/KBBackspaceLongPressHandler.m index 0a34511..4fc4b1c 100644 --- a/CustomKeyboard/Utils/KBBackspaceLongPressHandler.m +++ b/CustomKeyboard/Utils/KBBackspaceLongPressHandler.m @@ -7,6 +7,7 @@ #import "KBResponderUtils.h" #import "KBSkinManager.h" #import "KBBackspaceUndoManager.h" +#import "KBInputBufferManager.h" static const NSTimeInterval kKBBackspaceLongPressMinDuration = 0.35; static const NSTimeInterval kKBBackspaceRepeatInterval = 0.06; @@ -20,11 +21,11 @@ static const CGFloat kKBBackspaceClearLabelHeight = 26.0; static const CGFloat kKBBackspaceClearLabelPaddingX = 10.0; static const CGFloat kKBBackspaceClearLabelTopGap = 6.0; static const CGFloat kKBBackspaceClearLabelHorizontalInset = 6.0; -static const NSInteger kKBBackspaceClearBatchSize = 24; -static const NSTimeInterval kKBBackspaceClearBatchInterval = 0.005; +static const NSTimeInterval kKBBackspaceClearBatchInterval = 0.02; static const NSInteger kKBBackspaceClearMaxDeletes = 10000; static const NSInteger kKBBackspaceClearEmptyContextMaxRounds = 40; static const NSInteger kKBBackspaceClearMaxStep = 80; +static const NSInteger kKBBackspaceClearDeletesPerTick = 10; typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { KBBackspaceChunkClassUnknown = 0, @@ -34,6 +35,12 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { KBBackspaceChunkClassOther }; +typedef NS_ENUM(NSInteger, KBClearPhase) { + KBClearPhaseSkipWhitespace = 0, + KBClearPhaseSkipTrailingBoundary, + KBClearPhaseDeleteUntilBoundary +}; + @interface KBBackspaceLongPressHandler () @property (nonatomic, weak) UIView *containerView; @property (nonatomic, weak) UIView *backspaceButton; @@ -50,6 +57,7 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { @property (nonatomic, strong) UILabel *backspaceClearLabel; @property (nonatomic, copy) NSString *pendingClearBefore; @property (nonatomic, copy) NSString *pendingClearAfter; +@property (nonatomic, assign) KBClearPhase backspaceClearPhase; @end @implementation KBBackspaceLongPressHandler @@ -57,6 +65,7 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { - (instancetype)initWithContainerView:(UIView *)containerView { if (self = [super init]) { _containerView = containerView; + _backspaceClearPhase = KBClearPhaseSkipWhitespace; } return self; } @@ -103,9 +112,17 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { } switch (gr.state) { case UIGestureRecognizerStateBegan: { - [self kb_captureDeletionSnapshotIfNeeded]; + UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton); + UIInputViewController *ivc = KBFindInputViewController(start); + if (ivc) { + id proxy = ivc.textDocumentProxy; + [[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy]; + [[KBInputBufferManager shared] prepareSnapshotForDeleteWithContextBefore:proxy.documentContextBeforeInput + after:proxy.documentContextAfterInput]; + } if (self.showClearLabelEnabled) { [self kb_capturePendingClearSnapshotIfNeeded]; + [[KBInputBufferManager shared] beginPendingClearSnapshot]; } self.backspaceHoldToken += 1; NSUInteger token = self.backspaceHoldToken; @@ -141,6 +158,7 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { if (!ivc) { self.backspaceHoldActive = NO; return; } id proxy = ivc.textDocumentProxy; NSString *before = proxy.documentContextBeforeInput ?: @""; + if (before.length == 0) { before = [KBInputBufferManager shared].liveText ?: @""; } NSTimeInterval elapsed = [NSDate date].timeIntervalSinceReferenceDate - self.backspaceHoldStartTime; NSInteger deleteCount = 1; if (before.length > 0) { @@ -152,9 +170,8 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { [self kb_showBackspaceClearLabelIfNeeded]; } } - for (NSInteger i = 0; i < deleteCount; i++) { - [proxy deleteBackward]; - } + [[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:(NSUInteger)deleteCount]; + [[KBInputBufferManager shared] applyHoldDeleteCount:(NSUInteger)deleteCount]; NSTimeInterval interval = [self kb_backspaceRepeatIntervalForElapsed:elapsed]; __weak typeof(self) weakSelf = self; @@ -229,13 +246,16 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { - (NSInteger)kb_clearDeleteCountForContext:(NSString *)context hitBoundary:(BOOL *)hitBoundary { - if (context.length == 0) { return kKBBackspaceClearBatchSize; } + if (context.length == 0) { + if (hitBoundary) { *hitBoundary = NO; } + return 1; + } static NSCharacterSet *sentenceBoundarySet = nil; static NSCharacterSet *whitespaceSet = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;。!?;…\n"]; + sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?。!?"]; whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet]; }); @@ -310,6 +330,12 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { shouldClear = [self kb_isPointInsideBackspaceClearLabel:point]; } } +#if DEBUG + NSLog(@"[kb_handleBackspaceLongPressEnded] shouldClear=%@ highlighted=%@ labelHidden=%@", + shouldClear ? @"YES" : @"NO", + self.backspaceClearHighlighted ? @"YES" : @"NO", + self.backspaceClearLabel.hidden ? @"YES" : @"NO"); +#endif self.backspaceHoldActive = NO; self.backspaceChunkModeActive = NO; self.backspaceHoldToken += 1; @@ -320,6 +346,8 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { } else { self.pendingClearBefore = nil; self.pendingClearAfter = nil; + [[KBInputBufferManager shared] clearPendingClearSnapshot]; + [[KBInputBufferManager shared] commitLiveToManual]; } } @@ -411,7 +439,7 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { - (UILabel *)backspaceClearLabel { if (!_backspaceClearLabel) { UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero]; - label.text = @"立刻清空"; + label.text = @"上滑清空"; label.textAlignment = NSTextAlignmentCenter; label.font = [UIFont systemFontOfSize:12 weight:UIFontWeightSemibold]; label.textColor = [KBSkinManager shared].current.keyTextColor ?: UIColor.blackColor; @@ -431,13 +459,14 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton); UIInputViewController *ivc = KBFindInputViewController(start); if (ivc) { - NSString *before = self.pendingClearBefore ?: (ivc.textDocumentProxy.documentContextBeforeInput ?: @""); - NSString *after = self.pendingClearAfter ?: (ivc.textDocumentProxy.documentContextAfterInput ?: @""); - [[KBBackspaceUndoManager shared] recordClearWithContextBefore:before after:after]; + id proxy = ivc.textDocumentProxy; + [[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy]; } self.pendingClearBefore = nil; self.pendingClearAfter = nil; + [[KBInputBufferManager shared] clearPendingClearSnapshot]; self.backspaceClearToken += 1; + self.backspaceClearPhase = KBClearPhaseSkipWhitespace; NSUInteger token = self.backspaceClearToken; [self kb_clearAllInputStepForToken:token guard:0 emptyRounds:0]; } @@ -450,40 +479,90 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { UIInputViewController *ivc = KBFindInputViewController(start); if (!ivc) { return; } id proxy = ivc.textDocumentProxy; - NSString *before = proxy.documentContextBeforeInput ?: @""; - NSInteger count = before.length; - NSInteger batch = 0; NSInteger nextEmptyRounds = emptyRounds; - BOOL hitBoundary = NO; - if (count > 0) { - batch = [self kb_clearDeleteCountForContext:before hitBoundary:&hitBoundary]; - nextEmptyRounds = 0; - } else { - batch = kKBBackspaceClearBatchSize; - nextEmptyRounds = emptyRounds + 1; - } - if (batch <= 0) { batch = 1; } + static NSCharacterSet *sentenceBoundarySet = nil; + static NSCharacterSet *whitespaceSet = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?。!?"]; + whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet]; + }); + KBClearPhase phase = self.backspaceClearPhase; - if (guard >= kKBBackspaceClearMaxDeletes || - nextEmptyRounds > kKBBackspaceClearEmptyContextMaxRounds) { + 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; + // 即使 context 为空,也尝试删一次(某些宿主延迟更新) + [[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1]; + [[KBInputBufferManager shared] applyClearDeleteCount:1]; + deletedThisTick += 1; + break; + } + nextEmptyRounds = 0; + + 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; } + + BOOL isWhitespace = ([lastChar rangeOfCharacterFromSet:whitespaceSet].location != NSNotFound); + BOOL isBoundary = ([lastChar rangeOfCharacterFromSet:sentenceBoundarySet].location != NSNotFound); + + if (phase == KBClearPhaseSkipWhitespace) { + if (isWhitespace) { + [[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1]; + [[KBInputBufferManager shared] applyClearDeleteCount:1]; + deletedThisTick += 1; + continue; + } + phase = KBClearPhaseSkipTrailingBoundary; + } + + if (phase == KBClearPhaseSkipTrailingBoundary) { + if (isBoundary) { + [[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1]; + [[KBInputBufferManager shared] applyClearDeleteCount:1]; + deletedThisTick += 1; + continue; + } + phase = KBClearPhaseDeleteUntilBoundary; + } + + // phase == DeleteUntilBoundary + if (isBoundary) { + shouldStop = YES; // 保留该句末符号 + break; + } + [[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; } - for (NSInteger i = 0; i < batch; i++) { - [proxy deleteBackward]; - } - - NSInteger nextGuard = guard + batch; - BOOL shouldContinue = NO; - if (count > 0 && !hitBoundary) { - if (count > batch) { - shouldContinue = YES; - } else if ([proxy hasText]) { - shouldContinue = YES; - } - } - - if (!shouldContinue) { return; } __weak typeof(self) weakSelf = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kKBBackspaceClearBatchInterval * NSEC_PER_SEC)), @@ -513,7 +592,6 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { } - (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); @@ -521,6 +599,10 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) { id proxy = ivc.textDocumentProxy; self.pendingClearBefore = proxy.documentContextBeforeInput ?: @""; self.pendingClearAfter = proxy.documentContextAfterInput ?: @""; +#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 } @end diff --git a/CustomKeyboard/Utils/KBBackspaceUndoManager.h b/CustomKeyboard/Utils/KBBackspaceUndoManager.h index 573dd4d..5e772eb 100644 --- a/CustomKeyboard/Utils/KBBackspaceUndoManager.h +++ b/CustomKeyboard/Utils/KBBackspaceUndoManager.h @@ -21,6 +21,9 @@ extern NSNotificationName const KBBackspaceUndoStateDidChangeNotification; /// 记录一次“立刻清空”删除的内容(基于 documentContextBeforeInput/AfterInput)。 - (void)recordClearWithContextBefore:(NSString *)before after:(NSString *)after; +/// 记录本次将被 deleteBackward 的内容,并执行 deleteBackward(支持多次累计,撤销时一次性插回)。 +- (void)captureAndDeleteBackwardFromProxy:(id)proxy count:(NSUInteger)count; + /// 在指定 responder 处执行撤销(向光标处插回删除的内容) - (void)performUndoFromResponder:(UIResponder *)responder; diff --git a/CustomKeyboard/Utils/KBBackspaceUndoManager.m b/CustomKeyboard/Utils/KBBackspaceUndoManager.m index 7cc4e3c..d97246d 100644 --- a/CustomKeyboard/Utils/KBBackspaceUndoManager.m +++ b/CustomKeyboard/Utils/KBBackspaceUndoManager.m @@ -5,13 +5,38 @@ #import "KBBackspaceUndoManager.h" #import "KBResponderUtils.h" +#import "KBInputBufferManager.h" NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspaceUndoStateDidChangeNotification"; +#if DEBUG +static NSString *KBLogString(NSString *tag, NSString *text) { + NSString *safeTag = tag ?: @""; + NSString *safeText = text ?: @""; + if (safeText.length <= 2000) { + return [NSString stringWithFormat:@"[%@] len=%lu text=%@", safeTag, (unsigned long)safeText.length, safeText]; + } + NSString *head = [safeText substringToIndex:800]; + NSString *tail = [safeText substringFromIndex:safeText.length - 800]; + return [NSString stringWithFormat:@"[%@] len=%lu head=%@ ... tail=%@", safeTag, (unsigned long)safeText.length, head, tail]; +} +#define KB_UNDO_LOG(tag, text) NSLog(@"%@", KBLogString((tag), (text))) +#else +#define KB_UNDO_LOG(tag, text) do {} while(0) +#endif + +typedef NS_ENUM(NSInteger, KBUndoSnapshotSource) { + KBUndoSnapshotSourceNone = 0, + KBUndoSnapshotSourceDeletionSnapshot, + KBUndoSnapshotSourceClear +}; + @interface KBBackspaceUndoManager () @property (nonatomic, copy) NSString *undoText; @property (nonatomic, assign) NSInteger undoAfterLength; @property (nonatomic, assign) BOOL hasUndo; +@property (nonatomic, assign) KBUndoSnapshotSource snapshotSource; +@property (nonatomic, strong) NSMutableArray *undoDeletedPieces; @end @implementation KBBackspaceUndoManager @@ -29,55 +54,189 @@ NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspa if (self = [super init]) { _undoText = @""; _undoAfterLength = 0; + _snapshotSource = KBUndoSnapshotSourceNone; + _undoDeletedPieces = [NSMutableArray array]; } return self; } +- (void)captureAndDeleteBackwardFromProxy:(id)proxy count:(NSUInteger)count { + if (!proxy || count == 0) { return; } + + NSString *selected = proxy.selectedText ?: @""; + NSString *ctxBefore = proxy.documentContextBeforeInput ?: @""; + NSString *ctxAfter = proxy.documentContextAfterInput ?: @""; + NSUInteger ctxLen = ctxBefore.length + ctxAfter.length; + BOOL isSelectAllLike = (selected.length > 0 && + (ctxLen == 0 || selected.length >= MAX((NSUInteger)40, ctxLen * 2))); + if (isSelectAllLike) { + // “全选删除”在微信/QQ中通常拿不到可靠的全文,因此禁用撤销,避免插回错误/不完整内容。 + if (self.hasUndo) { + [self registerNonClearAction]; + } +#if DEBUG + KB_UNDO_LOG(@"captureAndDelete/selectAllDisableUndo", selected); +#endif + [proxy deleteBackward]; + [[KBInputBufferManager shared] resetWithText:@""]; + return; + } + + if (!self.hasUndo) { + [self.undoDeletedPieces removeAllObjects]; + self.undoText = @""; + self.undoAfterLength = 0; + self.snapshotSource = KBUndoSnapshotSourceDeletionSnapshot; + [self kb_updateHasUndo:YES]; + } + + BOOL didAppend = NO; + NSString *lastObservedBefore = nil; + for (NSUInteger i = 0; i < count; i++) { + NSString *before = proxy.documentContextBeforeInput ?: @""; + if (before.length > 0) { + // 若宿主在同一 runloop 内不更新 context,则跳过记录,避免把同一个字符重复记录成“多句”。 + if (lastObservedBefore && [before isEqualToString:lastObservedBefore]) { + // still delete, but don't record + } else { + NSString *piece = [self kb_lastComposedCharacterFromString:before]; + if (piece.length > 0) { + [self.undoDeletedPieces addObject:piece]; + didAppend = YES; + } + lastObservedBefore = before; + } + } + [proxy deleteBackward]; + } + +#if DEBUG + if (didAppend) { + NSUInteger piecesCount = self.undoDeletedPieces.count; + if (piecesCount <= 20) { + KB_UNDO_LOG(@"captureAndDelete/undoInsertTextNow", [self kb_buildUndoInsertTextFromPieces]); + } else if (piecesCount % 50 == 0) { + NSString *lastPiece = self.undoDeletedPieces.lastObject ?: @""; + NSLog(@"[captureAndDelete/undoPieces] pieces=%lu lastPiece=%@", + (unsigned long)piecesCount, + lastPiece); + } + } +#endif +} + - (void)recordDeletionSnapshotBefore:(NSString *)before after:(NSString *)after { - if (self.undoText.length > 0) { return; } + if (self.hasUndo) { return; } + NSString *pending = [KBInputBufferManager shared].pendingClearSnapshot; + NSString *manual = [KBInputBufferManager shared].manualSnapshot; + NSString *fallbackText = (pending.length > 0) ? pending : ((manual.length > 0) ? manual : [KBInputBufferManager shared].liveText); + if (fallbackText.length > 0) { + self.undoText = fallbackText; + self.undoAfterLength = 0; + self.snapshotSource = KBUndoSnapshotSourceDeletionSnapshot; + KB_UNDO_LOG(@"recordDeletionSnapshot/fallback", self.undoText); + [self kb_updateHasUndo:YES]; + 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.snapshotSource = KBUndoSnapshotSourceDeletionSnapshot; + KB_UNDO_LOG(@"recordDeletionSnapshot/context", self.undoText); + [self kb_updateHasUndo: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; + NSString *pending = [KBInputBufferManager shared].pendingClearSnapshot; + NSString *manual = [KBInputBufferManager shared].manualSnapshot; + NSString *fallbackText = (pending.length > 0) ? pending : ((manual.length > 0) ? manual : [KBInputBufferManager shared].liveText); + + NSString *safeBefore = before ?: @""; + NSString *safeAfter = after ?: @""; + NSString *contextText = [[safeBefore stringByAppendingString:safeAfter] copy]; + + NSString *candidate = (fallbackText.length > 0) ? fallbackText : contextText; + NSInteger candidateAfterLen = (fallbackText.length > 0) ? 0 : (NSInteger)safeAfter.length; + + if (candidate.length == 0) { return; } + + KB_UNDO_LOG(@"recordClear/candidate", candidate); + + if (self.undoText.length > 0) { + if (self.snapshotSource == KBUndoSnapshotSourceClear) { + KB_UNDO_LOG(@"recordClear/ignored(alreadyClear)", self.undoText); + [self kb_updateHasUndo:YES]; + return; + } + if (self.snapshotSource == KBUndoSnapshotSourceDeletionSnapshot) { + if (candidate.length > self.undoText.length) { + self.undoText = candidate; + self.undoAfterLength = candidateAfterLen; + KB_UNDO_LOG(@"recordClear/upgradedFromDeletion", self.undoText); + } else { + KB_UNDO_LOG(@"recordClear/keepDeletionSnapshot", self.undoText); + } + self.snapshotSource = KBUndoSnapshotSourceClear; + [self kb_updateHasUndo:YES]; + return; } } - if (self.undoText.length == 0) { return; } + + self.undoText = candidate; + self.undoAfterLength = candidateAfterLen; + self.snapshotSource = KBUndoSnapshotSourceClear; + KB_UNDO_LOG(@"recordClear/set", self.undoText); [self kb_updateHasUndo:YES]; } - (void)performUndoFromResponder:(UIResponder *)responder { - if (self.undoText.length == 0) { return; } + if (!self.hasUndo) { return; } UIInputViewController *ivc = KBFindInputViewController(responder); if (!ivc) { return; } id proxy = ivc.textDocumentProxy; - [self kb_clearAllTextForProxy:proxy]; - [proxy insertText:self.undoText]; - if (self.undoAfterLength > 0 && - [proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) { - [proxy adjustTextPositionByCharacterOffset:-self.undoAfterLength]; + NSString *curBefore = proxy.documentContextBeforeInput ?: @""; + NSString *curAfter = proxy.documentContextAfterInput ?: @""; + KB_UNDO_LOG(@"performUndo/currentBefore", curBefore); + KB_UNDO_LOG(@"performUndo/currentAfter", curAfter); + NSString *insertText = [self kb_buildUndoInsertTextFromPieces]; + if (insertText.length > 0) { + KB_UNDO_LOG(@"performUndo/insertDeletedText", insertText); + [proxy insertText:insertText]; + [[KBInputBufferManager shared] appendText:insertText]; + } else if (self.undoText.length > 0) { + KB_UNDO_LOG(@"performUndo/fallbackUndoText", self.undoText); + [self kb_clearAllTextForProxy:proxy]; + [proxy insertText:self.undoText]; + if (self.undoAfterLength > 0 && + [proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) { + [proxy adjustTextPositionByCharacterOffset:-self.undoAfterLength]; + } + [[KBInputBufferManager shared] resetWithText:self.undoText]; + } else { + return; } - self.undoText = @""; self.undoAfterLength = 0; + self.snapshotSource = KBUndoSnapshotSourceNone; + [self.undoDeletedPieces removeAllObjects]; [self kb_updateHasUndo:NO]; } - (void)registerNonClearAction { - if (self.undoText.length == 0) { return; } + if (!self.hasUndo) { return; } + if (self.undoText.length > 0) { + KB_UNDO_LOG(@"registerNonClearAction/clearUndoText", self.undoText); + } + if (self.undoDeletedPieces.count > 0) { + KB_UNDO_LOG(@"registerNonClearAction/clearDeletedPieces", [self kb_buildUndoInsertTextFromPieces]); + } self.undoText = @""; self.undoAfterLength = 0; + self.snapshotSource = KBUndoSnapshotSourceNone; + [self.undoDeletedPieces removeAllObjects]; [self kb_updateHasUndo:NO]; } @@ -89,6 +248,29 @@ NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspa [[NSNotificationCenter defaultCenter] postNotificationName:KBBackspaceUndoStateDidChangeNotification object:self]; } +- (NSString *)kb_lastComposedCharacterFromString:(NSString *)text { + if (text.length == 0) { return @""; } + __block NSString *last = @""; + [text enumerateSubstringsInRange:NSMakeRange(0, text.length) + options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse + usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) { + last = substring ?: @""; + *stop = YES; + }]; + return last ?: @""; + } + +- (NSString *)kb_buildUndoInsertTextFromPieces { + if (self.undoDeletedPieces.count == 0) { return @""; } + NSMutableString *result = [NSMutableString string]; + for (NSInteger i = (NSInteger)self.undoDeletedPieces.count - 1; i >= 0; i--) { + NSString *piece = self.undoDeletedPieces[(NSUInteger)i] ?: @""; + if (piece.length == 0) { continue; } + [result appendString:piece]; + } + return result; + } + static const NSInteger kKBUndoClearMaxRounds = 200; - (void)kb_clearAllTextForProxy:(id)proxy { diff --git a/CustomKeyboard/Utils/KBInputBufferManager.h b/CustomKeyboard/Utils/KBInputBufferManager.h new file mode 100644 index 0000000..c0368dd --- /dev/null +++ b/CustomKeyboard/Utils/KBInputBufferManager.h @@ -0,0 +1,34 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol UITextDocumentProxy; + +@interface KBInputBufferManager : NSObject + ++ (instancetype)shared; + +@property (nonatomic, copy, readonly) NSString *liveText; +@property (nonatomic, copy, readonly) NSString *manualSnapshot; +@property (nonatomic, copy, readonly) NSString *pendingClearSnapshot; + +- (void)seedIfEmptyWithContextBefore:(nullable NSString *)before after:(nullable NSString *)after; +- (void)updateFromExternalContextBefore:(nullable NSString *)before after:(nullable NSString *)after; +- (void)refreshFromProxyIfPossible:(nullable id)proxy; +- (void)prepareSnapshotForDeleteWithContextBefore:(nullable NSString *)before + after:(nullable NSString *)after; +- (void)beginPendingClearSnapshot; +- (void)clearPendingClearSnapshot; +- (void)resetWithText:(NSString *)text; +- (void)appendText:(NSString *)text; +- (void)deleteBackwardByCount:(NSUInteger)count; +- (void)replaceTailWithText:(NSString *)text deleteCount:(NSUInteger)count; +- (void)applyHoldDeleteCount:(NSUInteger)count; +- (void)applyClearDeleteCount:(NSUInteger)count; +- (void)clearAllLiveText; +- (void)commitLiveToManual; +- (void)restoreManualSnapshot; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CustomKeyboard/Utils/KBInputBufferManager.m b/CustomKeyboard/Utils/KBInputBufferManager.m new file mode 100644 index 0000000..95ab6c5 --- /dev/null +++ b/CustomKeyboard/Utils/KBInputBufferManager.m @@ -0,0 +1,279 @@ +#import "KBInputBufferManager.h" +#import + +#if DEBUG +static NSString *KBLogString2(NSString *tag, NSString *text) { + NSString *safeTag = tag ?: @""; + NSString *safeText = text ?: @""; + if (safeText.length <= 2000) { + return [NSString stringWithFormat:@"[%@] len=%lu text=%@", safeTag, (unsigned long)safeText.length, safeText]; + } + NSString *head = [safeText substringToIndex:800]; + NSString *tail = [safeText substringFromIndex:safeText.length - 800]; + return [NSString stringWithFormat:@"[%@] len=%lu head=%@ ... tail=%@", safeTag, (unsigned long)safeText.length, head, tail]; +} +#define KB_BUF_LOG(tag, text) NSLog(@"❤️=%@", KBLogString2((tag), (text))) +#else +#define KB_BUF_LOG(tag, text) do {} while(0) +#endif + +@interface KBInputBufferManager () +@property (nonatomic, copy, readwrite) NSString *liveText; +@property (nonatomic, copy, readwrite) NSString *manualSnapshot; +@property (nonatomic, copy, readwrite) NSString *pendingClearSnapshot; +@property (nonatomic, assign) BOOL manualSnapshotDirty; +@end + +@implementation KBInputBufferManager + ++ (instancetype)shared { + static KBInputBufferManager *mgr = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + mgr = [[KBInputBufferManager alloc] init]; + }); + return mgr; +} + +- (instancetype)init { + if (self = [super init]) { + _liveText = @""; + _manualSnapshot = @""; + _pendingClearSnapshot = @""; + _manualSnapshotDirty = NO; + } + return self; +} + +- (void)seedIfEmptyWithContextBefore:(NSString *)before after:(NSString *)after { + if (self.liveText.length > 0 || self.manualSnapshot.length > 0) { return; } + NSString *safeBefore = before ?: @""; + NSString *safeAfter = after ?: @""; + NSString *full = [safeBefore stringByAppendingString:safeAfter]; + if (full.length == 0) { return; } + self.liveText = full; + self.manualSnapshot = full; + self.manualSnapshotDirty = NO; + KB_BUF_LOG(@"seedIfEmpty", full); +} + +- (void)updateFromExternalContextBefore:(NSString *)before after:(NSString *)after { + NSString *safeBefore = before ?: @""; + NSString *safeAfter = after ?: @""; + NSString *context = [safeBefore stringByAppendingString:safeAfter]; + if (context.length == 0) { return; } + + // 微信/QQ 等宿主通常只提供光标附近“截断窗口”,不应当作为全文快照。 + // 这里只更新 liveText,给删除/清空逻辑做参考;manualSnapshot 仅由键盘自身输入/撤销来维护。 + self.liveText = context; + self.manualSnapshotDirty = YES; +#if DEBUG + static NSUInteger sExternalLogCounter = 0; + sExternalLogCounter += 1; + if (sExternalLogCounter % 12 == 0) { + KB_BUF_LOG(@"updateFromExternalContext/liveOnly", context); + } +#endif +} + +- (void)refreshFromProxyIfPossible:(id)proxy { + NSString *harvested = [self kb_harvestFullTextFromProxy:proxy]; + if (harvested.length == 0) { + KB_BUF_LOG(@"refreshFromProxy/failedOrUnsupported", @""); + return; + } + + BOOL manualEmpty = (self.manualSnapshot.length == 0); + BOOL longerThanManual = (harvested.length > self.manualSnapshot.length); + if (!(manualEmpty || longerThanManual)) { + KB_BUF_LOG(@"refreshFromProxy/ignoredShorter", harvested); + return; + } + + self.liveText = harvested; + self.manualSnapshot = harvested; + self.manualSnapshotDirty = NO; + KB_BUF_LOG(@"refreshFromProxy/accepted", harvested); +} + +- (void)prepareSnapshotForDeleteWithContextBefore:(NSString *)before + after:(NSString *)after { + NSString *safeBefore = before ?: @""; + NSString *safeAfter = after ?: @""; + NSString *context = [safeBefore stringByAppendingString:safeAfter]; + + BOOL manualValid = (self.manualSnapshot.length > 0 && + (context.length == 0 || + (self.manualSnapshot.length >= context.length && + [self.manualSnapshot rangeOfString:context].location != NSNotFound))); + if (manualValid) { return; } + + if (self.liveText.length > 0) { + self.manualSnapshot = self.liveText; + self.manualSnapshotDirty = NO; + KB_BUF_LOG(@"prepareSnapshotForDelete/fromLiveText", self.manualSnapshot); + return; + } + if (context.length > 0) { + self.manualSnapshot = context; + self.manualSnapshotDirty = NO; + KB_BUF_LOG(@"prepareSnapshotForDelete/fromContext", self.manualSnapshot); + } +} + +- (void)beginPendingClearSnapshot { + if (self.pendingClearSnapshot.length > 0) { return; } + if (self.manualSnapshot.length > 0) { + self.pendingClearSnapshot = self.manualSnapshot; + KB_BUF_LOG(@"beginPendingClearSnapshot/fromManual", self.pendingClearSnapshot); + return; + } + if (self.liveText.length > 0) { + self.pendingClearSnapshot = self.liveText; + KB_BUF_LOG(@"beginPendingClearSnapshot/fromLive", self.pendingClearSnapshot); + } +} + +- (void)clearPendingClearSnapshot { + self.pendingClearSnapshot = @""; +} + +- (void)resetWithText:(NSString *)text { + NSString *safe = text ?: @""; + self.liveText = safe; + self.manualSnapshot = safe; + self.pendingClearSnapshot = @""; + self.manualSnapshotDirty = NO; + KB_BUF_LOG(@"resetWithText", safe); +} + +- (void)appendText:(NSString *)text { + if (text.length == 0) { return; } + [self kb_syncManualSnapshotIfNeeded]; + self.liveText = [self.liveText stringByAppendingString:text]; + self.manualSnapshot = [self.manualSnapshot stringByAppendingString:text]; +} + +- (void)deleteBackwardByCount:(NSUInteger)count { + if (count == 0) { return; } + self.liveText = [self kb_stringByDeletingComposedCharacters:count from:self.liveText]; + self.manualSnapshot = [self kb_stringByDeletingComposedCharacters:count from:self.manualSnapshot]; +} + +- (void)replaceTailWithText:(NSString *)text deleteCount:(NSUInteger)count { + [self kb_syncManualSnapshotIfNeeded]; + [self deleteBackwardByCount:count]; + [self appendText:text]; +} + +- (void)applyHoldDeleteCount:(NSUInteger)count { + if (count == 0) { return; } + self.liveText = [self kb_stringByDeletingComposedCharacters:count from:self.liveText]; + self.manualSnapshotDirty = YES; +} + +- (void)applyClearDeleteCount:(NSUInteger)count { + if (count == 0) { return; } + self.liveText = [self kb_stringByDeletingComposedCharacters:count from:self.liveText]; + self.manualSnapshotDirty = YES; +} + +- (void)clearAllLiveText { + self.liveText = @""; + self.pendingClearSnapshot = @""; + self.manualSnapshotDirty = YES; +} + +- (void)commitLiveToManual { + self.manualSnapshot = self.liveText ?: @""; + self.manualSnapshotDirty = NO; + KB_BUF_LOG(@"commitLiveToManual", self.manualSnapshot); +} + +- (void)restoreManualSnapshot { + self.liveText = self.manualSnapshot ?: @""; +} + +#pragma mark - Helpers + +- (void)kb_syncManualSnapshotIfNeeded { + if (!self.manualSnapshotDirty) { return; } + self.manualSnapshot = self.liveText ?: @""; + self.manualSnapshotDirty = NO; +} + +- (NSString *)kb_stringByDeletingComposedCharacters:(NSUInteger)count + from:(NSString *)text { + if (count == 0) { return text ?: @""; } + NSString *source = text ?: @""; + if (source.length == 0) { return @""; } + + __block NSUInteger removed = 0; + __block NSUInteger endIndex = source.length; + [source enumerateSubstringsInRange:NSMakeRange(0, source.length) + options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse + usingBlock:^(__unused NSString *substring, NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) { + removed += 1; + endIndex = substringRange.location; + if (removed >= count) { + *stop = YES; + } + }]; + if (removed < count) { return @""; } + return [source substringToIndex:endIndex]; +} + +- (NSString *)kb_harvestFullTextFromProxy:(id)proxy { + if (!proxy) { return @""; } + if (![proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) { return @""; } + + static const NSInteger kKBHarvestMaxRounds = 160; + static const NSInteger kKBHarvestMaxChars = 50000; + + NSInteger movedToEnd = 0; + NSInteger movedLeft = 0; + NSMutableArray *chunks = [NSMutableArray array]; + NSInteger totalChars = 0; + + @try { + NSInteger guard = 0; + NSString *after = proxy.documentContextAfterInput ?: @""; + while (after.length > 0 && guard < kKBHarvestMaxRounds) { + NSInteger step = (NSInteger)after.length; + [(id)proxy adjustTextPositionByCharacterOffset:step]; + movedToEnd += step; + guard += 1; + after = proxy.documentContextAfterInput ?: @""; + } + + guard = 0; + NSString *before = proxy.documentContextBeforeInput ?: @""; + while (before.length > 0 && guard < kKBHarvestMaxRounds && totalChars < kKBHarvestMaxChars) { + [chunks addObject:before]; + totalChars += (NSInteger)before.length; + NSInteger step = (NSInteger)before.length; + [(id)proxy adjustTextPositionByCharacterOffset:-step]; + movedLeft += step; + guard += 1; + before = proxy.documentContextBeforeInput ?: @""; + } + } @finally { + if (movedLeft != 0) { + [(id)proxy adjustTextPositionByCharacterOffset:movedLeft]; + } + if (movedToEnd != 0) { + [(id)proxy adjustTextPositionByCharacterOffset:-movedToEnd]; + } + } + + if (chunks.count == 0) { return @""; } + NSMutableString *result = [NSMutableString stringWithCapacity:(NSUInteger)totalChars]; + for (NSInteger i = (NSInteger)chunks.count - 1; i >= 0; i--) { + NSString *part = chunks[(NSUInteger)i] ?: @""; + if (part.length == 0) { continue; } + [result appendString:part]; + } + return result; +} + +@end diff --git a/CustomKeyboard/View/KBFunctionView.m b/CustomKeyboard/View/KBFunctionView.m index b058db0..8b6fa66 100644 --- a/CustomKeyboard/View/KBFunctionView.m +++ b/CustomKeyboard/View/KBFunctionView.m @@ -27,6 +27,7 @@ #import "KBBizCode.h" #import "KBBackspaceLongPressHandler.h" #import "KBBackspaceUndoManager.h" +#import "KBInputBufferManager.h" @interface KBFunctionView () // UI @@ -721,6 +722,8 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C // - 无论允许/拒绝,都把本次 changeCount 记为已处理,避免一直重复询问。 - (void)startPasteboardMonitor { + // 禁用自动读取剪贴板,避免触发系统“允许粘贴”弹窗 + return; // 未开启“完全访问”时不做自动读取,避免宿主/系统拒绝并刷错误日志 if (![[KBFullAccessManager shared] hasFullAccess]) return; if (self.pasteboardTimer) return; @@ -775,9 +778,11 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C NSLog(@"点击:删除"); UIInputViewController *ivc = KBFindInputViewController(self); id proxy = ivc.textDocumentProxy; - [[KBBackspaceUndoManager shared] recordDeletionSnapshotBefore:proxy.documentContextBeforeInput - after:proxy.documentContextAfterInput]; - [proxy deleteBackward]; + [[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy]; + [[KBInputBufferManager shared] prepareSnapshotForDeleteWithContextBefore:proxy.documentContextBeforeInput + after:proxy.documentContextAfterInput]; + [[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1]; + [[KBInputBufferManager shared] applyHoldDeleteCount:1]; } - (void)onTapClear { NSLog(@"点击:清空"); @@ -791,6 +796,7 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C UIInputViewController *ivc = KBFindInputViewController(self); id proxy = ivc.textDocumentProxy; [proxy insertText:@"\n"]; + [[KBInputBufferManager shared] appendText:@"\n"]; } #pragma mark - Lazy diff --git a/CustomKeyboard/View/KBStreamTextView.m b/CustomKeyboard/View/KBStreamTextView.m index 96ea75e..93f258e 100644 --- a/CustomKeyboard/View/KBStreamTextView.m +++ b/CustomKeyboard/View/KBStreamTextView.m @@ -9,6 +9,7 @@ #import "KBStreamTextView.h" #import "KBResponderUtils.h" // 通过响应链找到 UIInputViewController,并将文本输出到宿主 +#import "KBInputBufferManager.h" @interface KBStreamTextView () @@ -371,6 +372,7 @@ static inline NSString *KBTrimRight(NSString *s) { if (rawText.length > 0) { [proxy insertText:rawText]; } + [[KBInputBufferManager shared] resetWithText:rawText ?: @""]; } } } diff --git a/_DerivedData/Logs/Build/LogStoreManifest.plist b/_DerivedData/Logs/Build/LogStoreManifest.plist new file mode 100644 index 0000000..f38de44 --- /dev/null +++ b/_DerivedData/Logs/Build/LogStoreManifest.plist @@ -0,0 +1,10 @@ + + + + + logFormatVersion + 11 + logs + + + diff --git a/_DerivedData/Logs/Launch/LogStoreManifest.plist b/_DerivedData/Logs/Launch/LogStoreManifest.plist new file mode 100644 index 0000000..f38de44 --- /dev/null +++ b/_DerivedData/Logs/Launch/LogStoreManifest.plist @@ -0,0 +1,10 @@ + + + + + logFormatVersion + 11 + logs + + + diff --git a/_DerivedData/Logs/Localization/LogStoreManifest.plist b/_DerivedData/Logs/Localization/LogStoreManifest.plist new file mode 100644 index 0000000..f38de44 --- /dev/null +++ b/_DerivedData/Logs/Localization/LogStoreManifest.plist @@ -0,0 +1,10 @@ + + + + + logFormatVersion + 11 + logs + + + diff --git a/_DerivedData/Logs/Package/800731DD-5595-43EC-B207-003BAB7870CE.xcactivitylog b/_DerivedData/Logs/Package/800731DD-5595-43EC-B207-003BAB7870CE.xcactivitylog new file mode 100644 index 0000000000000000000000000000000000000000..9a843475c9fc4f4b71499e711e3792abe3284db2 GIT binary patch literal 783 zcmV+q1MvJGiwFP!000006V+8qj@w2MJwfi!(7@0v++=^*Y^_H8C@?$$3{L`Nm7vvZ zN)1zNf*%<=fY&~NSKbF%#|JVD-y%(oY)CU6cm}o+gan9Uv8(FUdj;0j)!++>9lI+-l&k25XTJ6es+ttM5VPC~_sN|iWOoDI1 z06({?S>NSn0x?RDAW3}#BRQw>4U7l=p43~X)rj^0nqH7I&L0kXzK@R3WMO)}w)F~> zt=oWg-#6VX3|F>a56eKiO{kitGGX`9mOa$2s;=vHfj{z%;qg)v8Xapxk)g$r144+- z#@ib-nlOoo%qWF~W(cx;k-wXkc_2>#tW2Zdr~-?guivB?ZS=H;iX-+s@;XIT?Km}*Ux4XYFC$b zHMILN1GWRT>w(hRbe&yR2Doz35#Neti4x!<)au_pknX|N5Wsso5n?wMF>nl^a@ZKJCcx za~A7gZqiViD(l + + + + logFormatVersion + 11 + logs + + 800731DD-5595-43EC-B207-003BAB7870CE + + className + IDECommandLineBuildLog + documentTypeString + <nil> + domainType + Xcode.IDEActivityLogDomainType.BuildLog + fileName + 800731DD-5595-43EC-B207-003BAB7870CE.xcactivitylog + hasPrimaryLog + + primaryObservable + + highLevelStatus + E + totalNumberOfAnalyzerIssues + 0 + totalNumberOfErrors + 1 + totalNumberOfTestFailures + 0 + totalNumberOfWarnings + 3 + + signature + Resolve Packages + timeStartedRecording + 788359220.39837599 + timeStoppedRecording + 788359220.51885402 + title + Resolve Packages + uniqueIdentifier + 800731DD-5595-43EC-B207-003BAB7870CE + + + + diff --git a/_DerivedData/Logs/Test/LogStoreManifest.plist b/_DerivedData/Logs/Test/LogStoreManifest.plist new file mode 100644 index 0000000..f38de44 --- /dev/null +++ b/_DerivedData/Logs/Test/LogStoreManifest.plist @@ -0,0 +1,10 @@ + + + + + logFormatVersion + 11 + logs + + + diff --git a/_xcodebuild.xcresult/Data/data.0~KuxSmpdGJ92pn5u_BEjBWM8AjGpqkfeyzGLIsmXSHlanVADkRM6D7duevJ1pJ9aq75SfwN7YYm12kuCUDnZb4Q== b/_xcodebuild.xcresult/Data/data.0~KuxSmpdGJ92pn5u_BEjBWM8AjGpqkfeyzGLIsmXSHlanVADkRM6D7duevJ1pJ9aq75SfwN7YYm12kuCUDnZb4Q== new file mode 100644 index 0000000000000000000000000000000000000000..550dc6c636fc822b60bd40ad54927fcd82476e60 GIT binary patch literal 876 zcmV-y1C#tHwJ-euNF5shRy?&eU}@R_lVKR9xJ-?P1WAxUHPr4C+Z0+{ie|s1SkVH- zVdaF~+BSvusfjWvGf~FOn1TSHIHdrp0HpvIl50CIlHDLGB>F{CguV5xifMLea;nbv zsUH_fY3r;mgfGSqiLIX<@_-)NJ&CROrEYAU)t$PDq-)JJgs70{7sX_!$>wh zJER|nFUF6_e^z>Po*i8zCk~$-0-u~rj2j)ZS3Ilgk{n-DofF--NJ{y1;?I%Bd+G=x z1e}V@I#J*H^bnaW(Jz|dSyh+x*&+7Ur;TQ@7VADmc4(oqS~B)*U}3;B!tUG3B(~a1IT&MXp}$HN+1~opDe6+? zCu)McIAvLLsV-eat^;2s%hK8UKfosVe$Cf%%-mjzviodus!j*S7CL*S)+g(hPs}Bg zf=-Kh1$DDy_SdqBx~`+2gOZuESM5Zy^3M~g8@DD9zjChqMA9$(4XJw@8d6^21FX?; znD{hKi;shkan3#Hp7t=$iA;3OYa-*K^V*Be^FS#=$|kh5TytTI$xrwN&AVU)Q)=v{`NY2x(=uKy15+}$p!2c*z+^kiMp@+M+F-=h%Jr)UCbEurRQkozltfGp8ccUaYD4bAmC(78Z*I!vu{JBhE+3 zUYG2WNkIcrwv(@)FMam!<{}vYIsh2H7(26*O^*HTb*XhWQT2-Wbg6=Fm}=UFk#t0c zb4}wl8gx(^2iKr5Tmu!y@n{MJLC`h42lm>{MN&$r(L4uwI3A4`6^DC|@*EIU8pt>W zMi|`#Ho9S^dR+}8*&3q>XfH0>>jt|$NPNn3N__1#l2Xd}JjUZ7 zY?|mq=braO#^aoZd9MjJAfQ4(M|3$S!-g;f&QZ-}nbeMcGX3z2h=r1&(JAnq*+`>H zW=b5TG9=w4J*DAN84~f5p3*t`fXKX+NvJbtw*#NcDiF$5sBm&A2TPnA8w;}x?A<08 zyFqavhO&}%vAnBc>ckp4mbz9!SYx!y6XIZ;c-P9QXL6InfBnrCt5mjKT5<$0BPue(;1lxV% z2-OBE?zX!kDyY zdOQnhI6KCfT8g#^xW%g@5@w3q1S$+M5EfK8P*A}<9+-EVM6oAS90V{#;o5_dfkyLAaDjz|Dg$+_>o))Z literal 0 HcmV?d00001 diff --git a/_xcodebuild.xcresult/Data/data.0~Vu7KwkETF7p-NPaNF0LJw3Y7WlP-qb7sPA-2rXXX0yfpkv-0Nb-iYi6izT5RJ5yA2w_iANvsQaqTEGQDUvjnHA== b/_xcodebuild.xcresult/Data/data.0~Vu7KwkETF7p-NPaNF0LJw3Y7WlP-qb7sPA-2rXXX0yfpkv-0Nb-iYi6izT5RJ5yA2w_iANvsQaqTEGQDUvjnHA== new file mode 100644 index 0000000000000000000000000000000000000000..c23a2dfe272173aa4fac9bfb913a94da4ba9f7bb GIT binary patch literal 633 zcmV-<0*3u4wJ-euNQDysmLqT{;Bw3WP%V)^VFllXKee^`?-zLY3^H~0TRcU^7IcS% z6U;Ef3>y<*0AT=U02Ykag>M*r?B5ZTGXTTZ$9^?$y<$u61<(_`bYMEliDBz~t0Cv8 z?MKe}UCMFUdhFL{^ghO@3C47k-xKFY@1=Tzdy*4-y_Dm!WwXW74dSt1wyU2}4aJMa zbd(p@Mb;Z_s~K~x%RU-PFs55J&{2VJn$6fiOBR-KOZc&Wvu#vQ*K0da&iI*YU=O}w zv}WpC*AygP?rtSM_V0Q3hU| z7Al&@Ssz)3IPFZeqnw^uzn*bRPZ_s-4wN(Ci2xc%Vla^)gs4b44?+Y&fRu;?p(hd< z95|7xsvby*2MtLa0jDLtVY#asvR!?8v(}P?KGo=Z!(+cBc)NSOv9!_r`rF)P0n=~Z zDn;lB=cjKy_HPHuIiq#q-uKtNjC+zZg0`z)ZOwS)-gor_z3(C*h{DIsMWNTOj5JwQt7{LpM$qR%4fv0oD#%lu#0CECdN5aODUFM>JCbN{C zo#Obk1K)%;-PtmSN`a;x2zW0pJY8zWz7RnaD35pH?2ASr2mFKhcLR6(j@hZ%5o$Z& zrtrrgOq^A$egJXRc-)0C2=)hmDb0q#_>RuixpxEuvcwySgJ>t75R@1ZQU?<<7M%rx TVR7WEBFfPLCTq65gaP5p?ByoF literal 0 HcmV?d00001 diff --git a/_xcodebuild.xcresult/Data/data.0~vrHCUusPTfhGPOlPQjtQeI9gXBLbXl2rHlLgSQrEbpz8GKfnDMVRxt2ts61-uq5UX65K4l_LZXCoqTJ2PdnM1w== b/_xcodebuild.xcresult/Data/data.0~vrHCUusPTfhGPOlPQjtQeI9gXBLbXl2rHlLgSQrEbpz8GKfnDMVRxt2ts61-uq5UX65K4l_LZXCoqTJ2PdnM1w== new file mode 100644 index 0000000000000000000000000000000000000000..12adcc99b1fa59ab6f6779c3112df4276a76a6d3 GIT binary patch literal 193 zcmV;y06zaHwJ-euNW}#JvJ5LBK!<1mL!mK#Sdi63niyxzL#9I_#`<*uv$zY5VHk#c z0}v*yn>EJ*kk%YYbK2pWRu0BF&e1H)xdp(SNuC^eKLcbZO;G$3QJ4(|{f0Bhg9r7} zT5BmUCBoAIl_)V!K=IA|=VdQiGGTO|hEZmg;;)*rjV$MKrhSQ0Hk|UOVH{a4ft3d4DxD9X z{ab=jW~X>+8do`&bHrCJd#5GnT+Xy9QOX;#5gEsBrYW9H?A^WE66IGef90}w-~=Z* zniyl6%J$N~W>=!b@V&faFa3Gl&PHT=>0g+ZKn(k77&R=7HG`@c1xJG73I=6B0#A+u zCIVYzl!RX(;7l}lEC)akGsEJOA?g^Y;4C^i*>pBCib&Mq$-qz`wRFK~!zlf-jy>d^ zzJ$D!-P)8WrIZWUJM8%xELORjd7ZT|rDLS(zokzEtHrWVQA^F@RKPtdz>>B}s&w`SgZZ=s)NkZdpg%WJ*Y z<3m)GBVM;HnDxFyDKY=-T!(l5+J;qCedTh6Rmy>Snxd4_aveMC=CfP&QecH4U=MT` z8V-bm)iF7_tH98Y2x3N*M2E5xPzo(TAD{~9(Q#~{k>v4sFnnM(xdQ5VY-&cOfJ7uQ zAw5*gO~qrYJXj7KqFCW7jgWWpXaBZD`FMVHEzB#Op_WXU`}w&kQRdHXy;?DSsrbre z$1P1!%6w7$P9d2tir=-aL@A}c^e?ZnJ286N(Z4WlnY3wp5~aQLsih&;_%^b7GQDSb zeoIr70|sykd^q)z(V*#w&`=KDY+B_Qe!7x^5=|5q+dv7c<7PYtCO&l%DT?QU>fjy| zB|m%w9SjPM4Mhk`f(_(=IaM}M31C<~pos=gc8PG#AK+}N#Rg&{WNk8cks*M)`4gM( zT2GSb!34OUGXN)6&nb zu^jHn6s06tBmojgK_uAHq$Oxk@Bya>1tr)Ph|zr-M(IB_J<+Izc~q#;pi@SGj1dgL zKrlgJux$W9f}>Rj0nzeqs{x4tqExgtKWFk6476fR{T|^* zfrqH4czL*s@e9r!HR=IOswdXG;`SCJGRf2(A@~@Y2e?{+<#Z3Qks`WC*7%^o4*7|!C21#jB09;_p;1l$g*Y99#*GR0OUl@gYDa% Zyq#$$)~fH6pTOSOVr_Pk;FJz_Tj6WTBhLT; literal 0 HcmV?d00001 diff --git a/_xcodebuild.xcresult/Data/refs.0~vrHCUusPTfhGPOlPQjtQeI9gXBLbXl2rHlLgSQrEbpz8GKfnDMVRxt2ts61-uq5UX65K4l_LZXCoqTJ2PdnM1w== b/_xcodebuild.xcresult/Data/refs.0~vrHCUusPTfhGPOlPQjtQeI9gXBLbXl2rHlLgSQrEbpz8GKfnDMVRxt2ts61-uq5UX65K4l_LZXCoqTJ2PdnM1w== new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/_xcodebuild.xcresult/Data/refs.0~z4eUyi7LNyiJgMc9YvhirRPEbAqQY1U8Utz3Zonm5K5gXqlevHrHNamc2oelL32RyN2c9x-M59B2wBAeP3TOAg== b/_xcodebuild.xcresult/Data/refs.0~z4eUyi7LNyiJgMc9YvhirRPEbAqQY1U8Utz3Zonm5K5gXqlevHrHNamc2oelL32RyN2c9x-M59B2wBAeP3TOAg== new file mode 100644 index 0000000000000000000000000000000000000000..6efd698a00a93daf82ea4f41b146c1a15ea1b6e4 GIT binary patch literal 199 zcmV;&066~xK>$@{>cbA8g%kLS^R6|Na9i`1XlCQ_a7#lK9w(R%tbhbTs>x@Sd#)l- zG1+-qTmRa(lA4p^LLkVfFjn?dKVd-tR_@BeK@%6cel+%t7edLyb~{>A{;9s~JP)?5 zb=T7;>5~7nHNT=_E~3poQ74>$+YjOZ+w4KAlMrMBQuyZ_K>)t7!cyxGP54GU=}$sC zP + + + + dateCreated + 2025-12-25T12:40:20Z + externalLocations + + rootId + + hash + 0~z4eUyi7LNyiJgMc9YvhirRPEbAqQY1U8Utz3Zonm5K5gXqlevHrHNamc2oelL32RyN2c9x-M59B2wBAeP3TOAg== + + storage + + backend + fileBacked2 + compression + standard + + version + + major + 3 + minor + 53 + + + diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index 72a0552..b37ba39 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -225,6 +225,7 @@ A1B2C4212EB4B7A100000001 /* KBKeyboardPermissionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C4222EB4B7A100000001 /* KBKeyboardPermissionManager.m */; }; A1B2C9032FBD000100000001 /* KBBackspaceLongPressHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C9022FBD000100000001 /* KBBackspaceLongPressHandler.m */; }; A1B2C9052FBD000200000001 /* KBBackspaceUndoManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C9042FBD000200000001 /* KBBackspaceUndoManager.m */; }; + A1B2C9092FBD000200000005 /* KBInputBufferManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C9082FBD000200000004 /* KBInputBufferManager.m */; }; A1B2D7022EB8C00100000001 /* KBLangTestVC.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2D7012EB8C00100000001 /* KBLangTestVC.m */; }; A1B2E1012EBC7AAA00000001 /* KBTopThreeView.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2E0022EBC7AAA00000001 /* KBTopThreeView.m */; }; A1B2E1022EBC7AAA00000001 /* HomeHotCell.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2E0042EBC7AAA00000001 /* HomeHotCell.m */; }; @@ -639,6 +640,8 @@ A1B2C9022FBD000100000001 /* KBBackspaceLongPressHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBBackspaceLongPressHandler.m; sourceTree = ""; }; A1B2C9032FBD000200000001 /* KBBackspaceUndoManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBBackspaceUndoManager.h; sourceTree = ""; }; A1B2C9042FBD000200000001 /* KBBackspaceUndoManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBBackspaceUndoManager.m; sourceTree = ""; }; + A1B2C9072FBD000200000003 /* KBInputBufferManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBInputBufferManager.h; sourceTree = ""; }; + A1B2C9082FBD000200000004 /* KBInputBufferManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBInputBufferManager.m; sourceTree = ""; }; A1B2D7002EB8C00100000001 /* KBLangTestVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBLangTestVC.h; sourceTree = ""; }; A1B2D7012EB8C00100000001 /* KBLangTestVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBLangTestVC.m; sourceTree = ""; }; A1B2E0012EBC7AAA00000001 /* KBTopThreeView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBTopThreeView.h; sourceTree = ""; }; @@ -841,6 +844,8 @@ A1B2C9022FBD000100000001 /* KBBackspaceLongPressHandler.m */, A1B2C9032FBD000200000001 /* KBBackspaceUndoManager.h */, A1B2C9042FBD000200000001 /* KBBackspaceUndoManager.m */, + A1B2C9072FBD000200000003 /* KBInputBufferManager.h */, + A1B2C9082FBD000200000004 /* KBInputBufferManager.m */, ); path = Utils; sourceTree = ""; @@ -1947,6 +1952,7 @@ 04FC95702EB09516007BD342 /* KBFunctionView.m in Sources */, A1B2C9032FBD000100000001 /* KBBackspaceLongPressHandler.m in Sources */, A1B2C9052FBD000200000001 /* KBBackspaceUndoManager.m in Sources */, + A1B2C9092FBD000200000005 /* KBInputBufferManager.m in Sources */, 049FB23F2EC4B6EF00FAB05D /* KBULBridgeNotification.m in Sources */, 04791F992ED49CE7004E8522 /* KBFont.m in Sources */, 04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */, diff --git a/keyBoard/Class/Pay/M/KBPayProductModel.m b/keyBoard/Class/Pay/M/KBPayProductModel.m index 19fd65f..9a707cb 100644 --- a/keyBoard/Class/Pay/M/KBPayProductModel.m +++ b/keyBoard/Class/Pay/M/KBPayProductModel.m @@ -17,12 +17,12 @@ - (NSString *)coinsDisplayText { NSString *name = self.name ?: @""; - NSString *unit = self.unit ?: @""; - if (name.length && unit.length) { - return [NSString stringWithFormat:@"%@ %@", name, unit]; +// NSString *unit = self.unit ?: @""; + if (name.length) { + return [NSString stringWithFormat:@"%@", name]; } if (name.length) { return name; } - if (unit.length) { return unit; } +// if (unit.length) { return unit; } return @""; }