修改键盘长按立即清空和撤销删除
This commit is contained in:
@@ -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<UITextInput>)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
|
||||
[[KBInputBufferManager shared] refreshFromProxyIfPossible:self.textDocumentProxy];
|
||||
[[KBInputBufferManager shared] prepareSnapshotForDeleteWithContextBefore:self.textDocumentProxy.documentContextBeforeInput
|
||||
after:self.textDocumentProxy.documentContextAfterInput];
|
||||
[self.textDocumentProxy deleteBackward];
|
||||
[[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
|
||||
|
||||
@@ -11,10 +11,10 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
- (instancetype)initWithContainerView:(UIView *)containerView;
|
||||
|
||||
/// 配置删除按钮(包含长按删除;可选是否显示“立刻清空”提示)
|
||||
/// 配置删除按钮(包含长按删除;可选是否显示“上滑清空”提示)
|
||||
- (void)bindDeleteButton:(nullable UIView *)button showClearLabel:(BOOL)showClearLabel;
|
||||
|
||||
/// 触发“立刻清空”逻辑(可用于功能面板的清空按钮)
|
||||
/// 触发“上滑清空”逻辑(可用于功能面板的清空按钮)
|
||||
- (void)performClearAction;
|
||||
|
||||
@end
|
||||
|
||||
@@ -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<UITextDocumentProxy> 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<UITextDocumentProxy> 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<UITextDocumentProxy> 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<UITextDocumentProxy> 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<UITextDocumentProxy> 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
|
||||
|
||||
@@ -21,6 +21,9 @@ extern NSNotificationName const KBBackspaceUndoStateDidChangeNotification;
|
||||
/// 记录一次“立刻清空”删除的内容(基于 documentContextBeforeInput/AfterInput)。
|
||||
- (void)recordClearWithContextBefore:(NSString *)before after:(NSString *)after;
|
||||
|
||||
/// 记录本次将被 deleteBackward 的内容,并执行 deleteBackward(支持多次累计,撤销时一次性插回)。
|
||||
- (void)captureAndDeleteBackwardFromProxy:(id<UITextDocumentProxy>)proxy count:(NSUInteger)count;
|
||||
|
||||
/// 在指定 responder 处执行撤销(向光标处插回删除的内容)
|
||||
- (void)performUndoFromResponder:(UIResponder *)responder;
|
||||
|
||||
|
||||
@@ -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<NSString *> *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<UITextDocumentProxy>)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 *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 *full = [safeBefore stringByAppendingString:safeAfter];
|
||||
if (full.length > 0) {
|
||||
self.undoText = full;
|
||||
self.undoAfterLength = (NSInteger)safeAfter.length;
|
||||
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<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
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<UITextDocumentProxy>)proxy {
|
||||
|
||||
34
CustomKeyboard/Utils/KBInputBufferManager.h
Normal file
34
CustomKeyboard/Utils/KBInputBufferManager.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
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<UITextDocumentProxy>)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
|
||||
279
CustomKeyboard/Utils/KBInputBufferManager.m
Normal file
279
CustomKeyboard/Utils/KBInputBufferManager.m
Normal file
@@ -0,0 +1,279 @@
|
||||
#import "KBInputBufferManager.h"
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#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<UITextDocumentProxy>)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<UITextDocumentProxy>)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<NSString *> *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
|
||||
@@ -27,6 +27,7 @@
|
||||
#import "KBBizCode.h"
|
||||
#import "KBBackspaceLongPressHandler.h"
|
||||
#import "KBBackspaceUndoManager.h"
|
||||
#import "KBInputBufferManager.h"
|
||||
|
||||
@interface KBFunctionView () <KBFunctionBarViewDelegate, KBStreamOverlayViewDelegate, KBFunctionTagListViewDelegate>
|
||||
// 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<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
[[KBBackspaceUndoManager shared] recordDeletionSnapshotBefore:proxy.documentContextBeforeInput
|
||||
[[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy];
|
||||
[[KBInputBufferManager shared] prepareSnapshotForDeleteWithContextBefore:proxy.documentContextBeforeInput
|
||||
after:proxy.documentContextAfterInput];
|
||||
[proxy deleteBackward];
|
||||
[[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<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
[proxy insertText:@"\n"];
|
||||
[[KBInputBufferManager shared] appendText:@"\n"];
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
@@ -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 ?: @""];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
_DerivedData/Logs/Build/LogStoreManifest.plist
Normal file
10
_DerivedData/Logs/Build/LogStoreManifest.plist
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>logFormatVersion</key>
|
||||
<integer>11</integer>
|
||||
<key>logs</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
</plist>
|
||||
10
_DerivedData/Logs/Launch/LogStoreManifest.plist
Normal file
10
_DerivedData/Logs/Launch/LogStoreManifest.plist
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>logFormatVersion</key>
|
||||
<integer>11</integer>
|
||||
<key>logs</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
</plist>
|
||||
10
_DerivedData/Logs/Localization/LogStoreManifest.plist
Normal file
10
_DerivedData/Logs/Localization/LogStoreManifest.plist
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>logFormatVersion</key>
|
||||
<integer>11</integer>
|
||||
<key>logs</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
</plist>
|
||||
Binary file not shown.
47
_DerivedData/Logs/Package/LogStoreManifest.plist
Normal file
47
_DerivedData/Logs/Package/LogStoreManifest.plist
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>logFormatVersion</key>
|
||||
<integer>11</integer>
|
||||
<key>logs</key>
|
||||
<dict>
|
||||
<key>800731DD-5595-43EC-B207-003BAB7870CE</key>
|
||||
<dict>
|
||||
<key>className</key>
|
||||
<string>IDECommandLineBuildLog</string>
|
||||
<key>documentTypeString</key>
|
||||
<string><nil></string>
|
||||
<key>domainType</key>
|
||||
<string>Xcode.IDEActivityLogDomainType.BuildLog</string>
|
||||
<key>fileName</key>
|
||||
<string>800731DD-5595-43EC-B207-003BAB7870CE.xcactivitylog</string>
|
||||
<key>hasPrimaryLog</key>
|
||||
<true/>
|
||||
<key>primaryObservable</key>
|
||||
<dict>
|
||||
<key>highLevelStatus</key>
|
||||
<string>E</string>
|
||||
<key>totalNumberOfAnalyzerIssues</key>
|
||||
<integer>0</integer>
|
||||
<key>totalNumberOfErrors</key>
|
||||
<integer>1</integer>
|
||||
<key>totalNumberOfTestFailures</key>
|
||||
<integer>0</integer>
|
||||
<key>totalNumberOfWarnings</key>
|
||||
<integer>3</integer>
|
||||
</dict>
|
||||
<key>signature</key>
|
||||
<string>Resolve Packages</string>
|
||||
<key>timeStartedRecording</key>
|
||||
<real>788359220.39837599</real>
|
||||
<key>timeStoppedRecording</key>
|
||||
<real>788359220.51885402</real>
|
||||
<key>title</key>
|
||||
<string>Resolve Packages</string>
|
||||
<key>uniqueIdentifier</key>
|
||||
<string>800731DD-5595-43EC-B207-003BAB7870CE</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
10
_DerivedData/Logs/Test/LogStoreManifest.plist
Normal file
10
_DerivedData/Logs/Test/LogStoreManifest.plist
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>logFormatVersion</key>
|
||||
<integer>11</integer>
|
||||
<key>logs</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
</plist>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
29
_xcodebuild.xcresult/Info.plist
Normal file
29
_xcodebuild.xcresult/Info.plist
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>dateCreated</key>
|
||||
<date>2025-12-25T12:40:20Z</date>
|
||||
<key>externalLocations</key>
|
||||
<array/>
|
||||
<key>rootId</key>
|
||||
<dict>
|
||||
<key>hash</key>
|
||||
<string>0~z4eUyi7LNyiJgMc9YvhirRPEbAqQY1U8Utz3Zonm5K5gXqlevHrHNamc2oelL32RyN2c9x-M59B2wBAeP3TOAg==</string>
|
||||
</dict>
|
||||
<key>storage</key>
|
||||
<dict>
|
||||
<key>backend</key>
|
||||
<string>fileBacked2</string>
|
||||
<key>compression</key>
|
||||
<string>standard</string>
|
||||
</dict>
|
||||
<key>version</key>
|
||||
<dict>
|
||||
<key>major</key>
|
||||
<integer>3</integer>
|
||||
<key>minor</key>
|
||||
<integer>53</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -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 = "<group>"; };
|
||||
A1B2C9032FBD000200000001 /* KBBackspaceUndoManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBBackspaceUndoManager.h; sourceTree = "<group>"; };
|
||||
A1B2C9042FBD000200000001 /* KBBackspaceUndoManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBBackspaceUndoManager.m; sourceTree = "<group>"; };
|
||||
A1B2C9072FBD000200000003 /* KBInputBufferManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBInputBufferManager.h; sourceTree = "<group>"; };
|
||||
A1B2C9082FBD000200000004 /* KBInputBufferManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBInputBufferManager.m; sourceTree = "<group>"; };
|
||||
A1B2D7002EB8C00100000001 /* KBLangTestVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBLangTestVC.h; sourceTree = "<group>"; };
|
||||
A1B2D7012EB8C00100000001 /* KBLangTestVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBLangTestVC.m; sourceTree = "<group>"; };
|
||||
A1B2E0012EBC7AAA00000001 /* KBTopThreeView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBTopThreeView.h; sourceTree = "<group>"; };
|
||||
@@ -841,6 +844,8 @@
|
||||
A1B2C9022FBD000100000001 /* KBBackspaceLongPressHandler.m */,
|
||||
A1B2C9032FBD000200000001 /* KBBackspaceUndoManager.h */,
|
||||
A1B2C9042FBD000200000001 /* KBBackspaceUndoManager.m */,
|
||||
A1B2C9072FBD000200000003 /* KBInputBufferManager.h */,
|
||||
A1B2C9082FBD000200000004 /* KBInputBufferManager.m */,
|
||||
);
|
||||
path = Utils;
|
||||
sourceTree = "<group>";
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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 @"";
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user