Compare commits
3 Commits
2d5919016f
...
6ec98468de
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ec98468de | |||
| ae37730da6 | |||
| 203f104ece |
@@ -20,6 +20,7 @@
|
|||||||
#import "KBKeyboardSubscriptionView.h"
|
#import "KBKeyboardSubscriptionView.h"
|
||||||
#import "KBKeyboardSubscriptionProduct.h"
|
#import "KBKeyboardSubscriptionProduct.h"
|
||||||
#import "KBBackspaceUndoManager.h"
|
#import "KBBackspaceUndoManager.h"
|
||||||
|
#import "KBInputBufferManager.h"
|
||||||
#import "KBSuggestionEngine.h"
|
#import "KBSuggestionEngine.h"
|
||||||
|
|
||||||
// 提前声明一个类别,使编译器在 static 回调中识别 kb_consumePendingShopSkin 方法。
|
// 提前声明一个类别,使编译器在 static 回调中识别 kb_consumePendingShopSkin 方法。
|
||||||
@@ -64,6 +65,8 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
|
|
||||||
- (void)viewDidLoad {
|
- (void)viewDidLoad {
|
||||||
[super viewDidLoad];
|
[super viewDidLoad];
|
||||||
|
// 撤销删除是“上一段删除操作”的临时状态;键盘被系统回收/重建或跨页面回来时应当清空,避免误显示。
|
||||||
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
[self setupUI];
|
[self setupUI];
|
||||||
self.suggestionEngine = [KBSuggestionEngine shared];
|
self.suggestionEngine = [KBSuggestionEngine shared];
|
||||||
self.currentWord = @"";
|
self.currentWord = @"";
|
||||||
@@ -92,7 +95,24 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
|
|
||||||
- (void)viewWillAppear:(BOOL)animated{
|
- (void)viewWillAppear:(BOOL)animated{
|
||||||
[super viewWillAppear:animated];
|
[super viewWillAppear:animated];
|
||||||
|
// 进入/重新进入输入界面时,清理上一次会话残留的撤销状态与缓存,避免显示“撤销删除”但实际上已不可撤销。
|
||||||
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
|
[[KBInputBufferManager shared] resetWithText:@""];
|
||||||
[[KBLocalizationManager shared] reloadFromSharedStorageIfNeeded];
|
[[KBLocalizationManager shared] reloadFromSharedStorageIfNeeded];
|
||||||
|
// 注意:微信/QQ 等宿主的 documentContext 可能是“截断窗口”,这里只更新 liveText,不要把它当作全文 manualSnapshot。
|
||||||
|
[[KBInputBufferManager shared] updateFromExternalContextBefore:self.textDocumentProxy.documentContextBeforeInput
|
||||||
|
after:self.textDocumentProxy.documentContextAfterInput];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)viewWillDisappear:(BOOL)animated {
|
||||||
|
[super viewWillDisappear:animated];
|
||||||
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)textDidChange:(id<UITextInput>)textInput {
|
||||||
|
[super textDidChange:textInput];
|
||||||
|
[[KBInputBufferManager shared] updateFromExternalContextBefore:self.textDocumentProxy.documentContextBeforeInput
|
||||||
|
after:self.textDocumentProxy.documentContextAfterInput];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -369,22 +389,27 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
NSString *text = key.output ?: key.title ?: @"";
|
NSString *text = key.output ?: key.title ?: @"";
|
||||||
[self.textDocumentProxy insertText:text];
|
[self.textDocumentProxy insertText:text];
|
||||||
[self kb_updateCurrentWordWithInsertedText:text];
|
[self kb_updateCurrentWordWithInsertedText:text];
|
||||||
|
[[KBInputBufferManager shared] appendText:text];
|
||||||
} break;
|
} break;
|
||||||
case KBKeyTypeBackspace:
|
case KBKeyTypeBackspace:
|
||||||
[[KBBackspaceUndoManager shared] recordDeletionSnapshotBefore:self.textDocumentProxy.documentContextBeforeInput
|
[[KBInputBufferManager shared] refreshFromProxyIfPossible:self.textDocumentProxy];
|
||||||
|
[[KBInputBufferManager shared] prepareSnapshotForDeleteWithContextBefore:self.textDocumentProxy.documentContextBeforeInput
|
||||||
after:self.textDocumentProxy.documentContextAfterInput];
|
after:self.textDocumentProxy.documentContextAfterInput];
|
||||||
[self.textDocumentProxy deleteBackward];
|
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:self.textDocumentProxy count:1];
|
||||||
[self kb_scheduleContextRefreshResetSuppression:NO];
|
[self kb_scheduleContextRefreshResetSuppression:NO];
|
||||||
|
[[KBInputBufferManager shared] applyHoldDeleteCount:1];
|
||||||
break;
|
break;
|
||||||
case KBKeyTypeSpace:
|
case KBKeyTypeSpace:
|
||||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
[self.textDocumentProxy insertText:@" "];
|
[self.textDocumentProxy insertText:@" "];
|
||||||
[self kb_clearCurrentWord];
|
[self kb_clearCurrentWord];
|
||||||
|
[[KBInputBufferManager shared] appendText:@" "];
|
||||||
break;
|
break;
|
||||||
case KBKeyTypeReturn:
|
case KBKeyTypeReturn:
|
||||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
[self.textDocumentProxy insertText:@"\n"];
|
[self.textDocumentProxy insertText:@"\n"];
|
||||||
[self kb_clearCurrentWord];
|
[self kb_clearCurrentWord];
|
||||||
|
[[KBInputBufferManager shared] appendText:@"\n"];
|
||||||
break;
|
break;
|
||||||
case KBKeyTypeGlobe:
|
case KBKeyTypeGlobe:
|
||||||
[self advanceToNextInputMode]; break;
|
[self advanceToNextInputMode]; break;
|
||||||
@@ -419,6 +444,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
[self.textDocumentProxy insertText:emoji];
|
[self.textDocumentProxy insertText:emoji];
|
||||||
[self kb_clearCurrentWord];
|
[self kb_clearCurrentWord];
|
||||||
|
[[KBInputBufferManager shared] appendText:emoji];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)keyBoardMainViewDidTapUndo:(KBKeyBoardMainView *)keyBoardMainView {
|
- (void)keyBoardMainViewDidTapUndo:(KBKeyBoardMainView *)keyBoardMainView {
|
||||||
@@ -432,6 +458,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
|
|
||||||
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectSuggestion:(NSString *)suggestion {
|
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didSelectSuggestion:(NSString *)suggestion {
|
||||||
if (suggestion.length == 0) { return; }
|
if (suggestion.length == 0) { return; }
|
||||||
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
NSString *current = self.currentWord ?: @"";
|
NSString *current = self.currentWord ?: @"";
|
||||||
if (current.length > 0) {
|
if (current.length > 0) {
|
||||||
for (NSUInteger i = 0; i < current.length; i++) {
|
for (NSUInteger i = 0; i < current.length; i++) {
|
||||||
@@ -443,6 +470,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
[self.suggestionEngine recordSelection:suggestion];
|
[self.suggestionEngine recordSelection:suggestion];
|
||||||
self.suppressSuggestions = YES;
|
self.suppressSuggestions = YES;
|
||||||
[self.keyBoardMainView kb_setSuggestions:@[]];
|
[self.keyBoardMainView kb_setSuggestions:@[]];
|
||||||
|
[[KBInputBufferManager shared] replaceTailWithText:suggestion deleteCount:current.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - KBFunctionViewDelegate
|
// MARK: - KBFunctionViewDelegate
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
|
|
||||||
- (instancetype)initWithContainerView:(UIView *)containerView;
|
- (instancetype)initWithContainerView:(UIView *)containerView;
|
||||||
|
|
||||||
/// 配置删除按钮(包含长按删除;可选是否显示“立刻清空”提示)
|
/// 配置删除按钮(包含长按删除;可选是否显示“上滑清空”提示)
|
||||||
- (void)bindDeleteButton:(nullable UIView *)button showClearLabel:(BOOL)showClearLabel;
|
- (void)bindDeleteButton:(nullable UIView *)button showClearLabel:(BOOL)showClearLabel;
|
||||||
|
|
||||||
/// 触发“立刻清空”逻辑(可用于功能面板的清空按钮)
|
/// 触发“上滑清空”逻辑(可用于功能面板的清空按钮)
|
||||||
- (void)performClearAction;
|
- (void)performClearAction;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -7,24 +7,25 @@
|
|||||||
#import "KBResponderUtils.h"
|
#import "KBResponderUtils.h"
|
||||||
#import "KBSkinManager.h"
|
#import "KBSkinManager.h"
|
||||||
#import "KBBackspaceUndoManager.h"
|
#import "KBBackspaceUndoManager.h"
|
||||||
|
#import "KBInputBufferManager.h"
|
||||||
|
|
||||||
static const NSTimeInterval kKBBackspaceLongPressMinDuration = 0.35;
|
static const NSTimeInterval kKBBackspaceLongPressMinDuration = 0.35;
|
||||||
static const NSTimeInterval kKBBackspaceRepeatInterval = 0.06;
|
static const NSTimeInterval kKBBackspaceRepeatInterval = 0.06;
|
||||||
static const NSTimeInterval kKBBackspaceChunkStartDelay = 1.0;
|
static const NSTimeInterval kKBBackspaceChunkStartDelay = 0.6;
|
||||||
static const NSTimeInterval kKBBackspaceChunkRepeatInterval = 0.1;
|
static const NSTimeInterval kKBBackspaceChunkRepeatInterval = 0.1;
|
||||||
static const NSTimeInterval kKBBackspaceChunkFastDelay = 1.4;
|
static const NSTimeInterval kKBBackspaceChunkFastDelay = 1.2;
|
||||||
static const NSInteger kKBBackspaceChunkSize = 6;
|
static const NSInteger kKBBackspaceChunkSize = 8;
|
||||||
static const NSInteger kKBBackspaceChunkSizeFast = 12;
|
static const NSInteger kKBBackspaceChunkSizeFast = 16;
|
||||||
static const CGFloat kKBBackspaceClearLabelCornerRadius = 8.0;
|
static const CGFloat kKBBackspaceClearLabelCornerRadius = 8.0;
|
||||||
static const CGFloat kKBBackspaceClearLabelHeight = 34;
|
static const CGFloat kKBBackspaceClearLabelHeight = 34;
|
||||||
static const CGFloat kKBBackspaceClearLabelPaddingX = 10.0;
|
static const CGFloat kKBBackspaceClearLabelPaddingX = 10.0;
|
||||||
static const CGFloat kKBBackspaceClearLabelTopGap = 6.0;
|
static const CGFloat kKBBackspaceClearLabelTopGap = 6.0;
|
||||||
static const CGFloat kKBBackspaceClearLabelHorizontalInset = 6.0;
|
static const CGFloat kKBBackspaceClearLabelHorizontalInset = 6.0;
|
||||||
static const NSInteger kKBBackspaceClearBatchSize = 24;
|
static const NSTimeInterval kKBBackspaceClearBatchInterval = 0.02;
|
||||||
static const NSTimeInterval kKBBackspaceClearBatchInterval = 0.005;
|
|
||||||
static const NSInteger kKBBackspaceClearMaxDeletes = 10000;
|
static const NSInteger kKBBackspaceClearMaxDeletes = 10000;
|
||||||
static const NSInteger kKBBackspaceClearEmptyContextMaxRounds = 40;
|
static const NSInteger kKBBackspaceClearEmptyContextMaxRounds = 40;
|
||||||
static const NSInteger kKBBackspaceClearMaxStep = 80;
|
static const NSInteger kKBBackspaceClearMaxStep = 80;
|
||||||
|
static const NSInteger kKBBackspaceClearDeletesPerTick = 10;
|
||||||
|
|
||||||
typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||||
KBBackspaceChunkClassUnknown = 0,
|
KBBackspaceChunkClassUnknown = 0,
|
||||||
@@ -34,6 +35,12 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
KBBackspaceChunkClassOther
|
KBBackspaceChunkClassOther
|
||||||
};
|
};
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, KBClearPhase) {
|
||||||
|
KBClearPhaseSkipWhitespace = 0,
|
||||||
|
KBClearPhaseSkipTrailingBoundary,
|
||||||
|
KBClearPhaseDeleteUntilBoundary
|
||||||
|
};
|
||||||
|
|
||||||
@interface KBBackspaceLongPressHandler ()
|
@interface KBBackspaceLongPressHandler ()
|
||||||
@property (nonatomic, weak) UIView *containerView;
|
@property (nonatomic, weak) UIView *containerView;
|
||||||
@property (nonatomic, weak) UIView *backspaceButton;
|
@property (nonatomic, weak) UIView *backspaceButton;
|
||||||
@@ -50,6 +57,7 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
@property (nonatomic, strong) UILabel *backspaceClearLabel;
|
@property (nonatomic, strong) UILabel *backspaceClearLabel;
|
||||||
@property (nonatomic, copy) NSString *pendingClearBefore;
|
@property (nonatomic, copy) NSString *pendingClearBefore;
|
||||||
@property (nonatomic, copy) NSString *pendingClearAfter;
|
@property (nonatomic, copy) NSString *pendingClearAfter;
|
||||||
|
@property (nonatomic, assign) KBClearPhase backspaceClearPhase;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation KBBackspaceLongPressHandler
|
@implementation KBBackspaceLongPressHandler
|
||||||
@@ -57,6 +65,7 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
- (instancetype)initWithContainerView:(UIView *)containerView {
|
- (instancetype)initWithContainerView:(UIView *)containerView {
|
||||||
if (self = [super init]) {
|
if (self = [super init]) {
|
||||||
_containerView = containerView;
|
_containerView = containerView;
|
||||||
|
_backspaceClearPhase = KBClearPhaseSkipWhitespace;
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
@@ -103,9 +112,17 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
}
|
}
|
||||||
switch (gr.state) {
|
switch (gr.state) {
|
||||||
case UIGestureRecognizerStateBegan: {
|
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) {
|
if (self.showClearLabelEnabled) {
|
||||||
[self kb_capturePendingClearSnapshotIfNeeded];
|
[self kb_capturePendingClearSnapshotIfNeeded];
|
||||||
|
[[KBInputBufferManager shared] beginPendingClearSnapshot];
|
||||||
}
|
}
|
||||||
self.backspaceHoldToken += 1;
|
self.backspaceHoldToken += 1;
|
||||||
NSUInteger token = self.backspaceHoldToken;
|
NSUInteger token = self.backspaceHoldToken;
|
||||||
@@ -141,6 +158,7 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
if (!ivc) { self.backspaceHoldActive = NO; return; }
|
if (!ivc) { self.backspaceHoldActive = NO; return; }
|
||||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
||||||
|
if (before.length == 0) { before = [KBInputBufferManager shared].liveText ?: @""; }
|
||||||
NSTimeInterval elapsed = [NSDate date].timeIntervalSinceReferenceDate - self.backspaceHoldStartTime;
|
NSTimeInterval elapsed = [NSDate date].timeIntervalSinceReferenceDate - self.backspaceHoldStartTime;
|
||||||
NSInteger deleteCount = 1;
|
NSInteger deleteCount = 1;
|
||||||
if (before.length > 0) {
|
if (before.length > 0) {
|
||||||
@@ -152,9 +170,8 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
[self kb_showBackspaceClearLabelIfNeeded];
|
[self kb_showBackspaceClearLabelIfNeeded];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (NSInteger i = 0; i < deleteCount; i++) {
|
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:(NSUInteger)deleteCount];
|
||||||
[proxy deleteBackward];
|
[[KBInputBufferManager shared] applyHoldDeleteCount:(NSUInteger)deleteCount];
|
||||||
}
|
|
||||||
|
|
||||||
NSTimeInterval interval = [self kb_backspaceRepeatIntervalForElapsed:elapsed];
|
NSTimeInterval interval = [self kb_backspaceRepeatIntervalForElapsed:elapsed];
|
||||||
__weak typeof(self) weakSelf = self;
|
__weak typeof(self) weakSelf = self;
|
||||||
@@ -193,34 +210,77 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||||||
asciiWordSet = [NSCharacterSet characterSetWithCharactersInString:
|
asciiWordSet = [NSCharacterSet characterSetWithCharactersInString:
|
||||||
@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"];
|
@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"];
|
||||||
punctuationSet = [NSCharacterSet punctuationCharacterSet];
|
NSMutableCharacterSet *punct = [[NSCharacterSet punctuationCharacterSet] mutableCopy];
|
||||||
|
// 补齐常见中文/全角标点(避免 chunk 总是只删 1 个符号)
|
||||||
|
[punct addCharactersInString:@",。!?;:、()【】《》“”‘’·…—"];
|
||||||
|
punctuationSet = [punct copy];
|
||||||
});
|
});
|
||||||
|
|
||||||
__block NSInteger deleteCount = 0;
|
__block NSInteger deleteCount = 0;
|
||||||
__block KBBackspaceChunkClass chunkClass = KBBackspaceChunkClassUnknown;
|
typedef NS_ENUM(NSInteger, KBBackspaceChunkPhase) {
|
||||||
|
KBBackspaceChunkPhaseWhitespace = 0,
|
||||||
|
KBBackspaceChunkPhasePunctuation,
|
||||||
|
KBBackspaceChunkPhaseCore
|
||||||
|
};
|
||||||
|
__block KBBackspaceChunkPhase phase = KBBackspaceChunkPhaseWhitespace;
|
||||||
|
__block KBBackspaceChunkClass coreClass = KBBackspaceChunkClassUnknown;
|
||||||
|
|
||||||
[context enumerateSubstringsInRange:NSMakeRange(0, context.length)
|
[context enumerateSubstringsInRange:NSMakeRange(0, context.length)
|
||||||
options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse
|
options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse
|
||||||
usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) {
|
usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) {
|
||||||
if (substring.length == 0) { return; }
|
if (substring.length == 0) { return; }
|
||||||
KBBackspaceChunkClass currentClass = KBBackspaceChunkClassOther;
|
if (deleteCount >= maxCount) {
|
||||||
if ([substring rangeOfCharacterFromSet:whitespaceSet].location != NSNotFound) {
|
|
||||||
currentClass = KBBackspaceChunkClassWhitespace;
|
|
||||||
} else if ([substring rangeOfCharacterFromSet:asciiWordSet].location != NSNotFound) {
|
|
||||||
currentClass = KBBackspaceChunkClassASCIIWord;
|
|
||||||
} else if ([substring rangeOfCharacterFromSet:punctuationSet].location != NSNotFound) {
|
|
||||||
currentClass = KBBackspaceChunkClassPunctuation;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chunkClass == KBBackspaceChunkClassUnknown) {
|
|
||||||
chunkClass = currentClass;
|
|
||||||
} else if (chunkClass != currentClass) {
|
|
||||||
*stop = YES;
|
*stop = YES;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KBBackspaceChunkClass currentClass = KBBackspaceChunkClassOther;
|
||||||
|
if ([substring rangeOfCharacterFromSet:whitespaceSet].location != NSNotFound) {
|
||||||
|
currentClass = KBBackspaceChunkClassWhitespace;
|
||||||
|
} else if ([substring rangeOfCharacterFromSet:punctuationSet].location != NSNotFound) {
|
||||||
|
currentClass = KBBackspaceChunkClassPunctuation;
|
||||||
|
} else if ([substring rangeOfCharacterFromSet:asciiWordSet].location != NSNotFound) {
|
||||||
|
currentClass = KBBackspaceChunkClassASCIIWord;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL consumed = NO;
|
||||||
|
while (!consumed) {
|
||||||
|
if (phase == KBBackspaceChunkPhaseWhitespace) {
|
||||||
|
if (currentClass == KBBackspaceChunkClassWhitespace) {
|
||||||
deleteCount += 1;
|
deleteCount += 1;
|
||||||
|
consumed = YES;
|
||||||
|
} else {
|
||||||
|
phase = KBBackspaceChunkPhasePunctuation;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phase == KBBackspaceChunkPhasePunctuation) {
|
||||||
|
if (currentClass == KBBackspaceChunkClassPunctuation) {
|
||||||
|
deleteCount += 1;
|
||||||
|
consumed = YES;
|
||||||
|
} else {
|
||||||
|
phase = KBBackspaceChunkPhaseCore;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// phase == Core:连续删同一类(ASCII 单词 / 其它),让效果更像微信“几个字一组”
|
||||||
|
if (coreClass == KBBackspaceChunkClassUnknown) {
|
||||||
|
coreClass = currentClass;
|
||||||
|
}
|
||||||
|
if (currentClass != coreClass) {
|
||||||
|
*stop = YES;
|
||||||
|
consumed = YES;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
deleteCount += 1;
|
||||||
|
consumed = YES;
|
||||||
|
}
|
||||||
|
|
||||||
if (deleteCount >= maxCount) {
|
if (deleteCount >= maxCount) {
|
||||||
*stop = YES;
|
*stop = YES;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
|
|
||||||
@@ -229,13 +289,16 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
|
|
||||||
- (NSInteger)kb_clearDeleteCountForContext:(NSString *)context
|
- (NSInteger)kb_clearDeleteCountForContext:(NSString *)context
|
||||||
hitBoundary:(BOOL *)hitBoundary {
|
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 *sentenceBoundarySet = nil;
|
||||||
static NSCharacterSet *whitespaceSet = nil;
|
static NSCharacterSet *whitespaceSet = nil;
|
||||||
static dispatch_once_t onceToken;
|
static dispatch_once_t onceToken;
|
||||||
dispatch_once(&onceToken, ^{
|
dispatch_once(&onceToken, ^{
|
||||||
sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;。!?;…\n"];
|
sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?。!?"];
|
||||||
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -310,6 +373,12 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
shouldClear = [self kb_isPointInsideBackspaceClearLabel:point];
|
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.backspaceHoldActive = NO;
|
||||||
self.backspaceChunkModeActive = NO;
|
self.backspaceChunkModeActive = NO;
|
||||||
self.backspaceHoldToken += 1;
|
self.backspaceHoldToken += 1;
|
||||||
@@ -320,6 +389,8 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
} else {
|
} else {
|
||||||
self.pendingClearBefore = nil;
|
self.pendingClearBefore = nil;
|
||||||
self.pendingClearAfter = nil;
|
self.pendingClearAfter = nil;
|
||||||
|
[[KBInputBufferManager shared] clearPendingClearSnapshot];
|
||||||
|
[[KBInputBufferManager shared] commitLiveToManual];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,13 +502,14 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||||
UIInputViewController *ivc = KBFindInputViewController(start);
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||||
if (ivc) {
|
if (ivc) {
|
||||||
NSString *before = self.pendingClearBefore ?: (ivc.textDocumentProxy.documentContextBeforeInput ?: @"");
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
NSString *after = self.pendingClearAfter ?: (ivc.textDocumentProxy.documentContextAfterInput ?: @"");
|
[[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy];
|
||||||
[[KBBackspaceUndoManager shared] recordClearWithContextBefore:before after:after];
|
|
||||||
}
|
}
|
||||||
self.pendingClearBefore = nil;
|
self.pendingClearBefore = nil;
|
||||||
self.pendingClearAfter = nil;
|
self.pendingClearAfter = nil;
|
||||||
|
[[KBInputBufferManager shared] clearPendingClearSnapshot];
|
||||||
self.backspaceClearToken += 1;
|
self.backspaceClearToken += 1;
|
||||||
|
self.backspaceClearPhase = KBClearPhaseSkipWhitespace;
|
||||||
NSUInteger token = self.backspaceClearToken;
|
NSUInteger token = self.backspaceClearToken;
|
||||||
[self kb_clearAllInputStepForToken:token guard:0 emptyRounds:0];
|
[self kb_clearAllInputStepForToken:token guard:0 emptyRounds:0];
|
||||||
}
|
}
|
||||||
@@ -450,40 +522,101 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
UIInputViewController *ivc = KBFindInputViewController(start);
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||||
if (!ivc) { return; }
|
if (!ivc) { return; }
|
||||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
|
||||||
NSInteger count = before.length;
|
|
||||||
NSInteger batch = 0;
|
|
||||||
NSInteger nextEmptyRounds = emptyRounds;
|
NSInteger nextEmptyRounds = emptyRounds;
|
||||||
BOOL hitBoundary = NO;
|
static NSCharacterSet *stopBoundarySet = nil;
|
||||||
if (count > 0) {
|
static NSCharacterSet *trailingBoundarySet = nil;
|
||||||
batch = [self kb_clearDeleteCountForContext:before hitBoundary:&hitBoundary];
|
static NSCharacterSet *trailingWhitespaceSet = nil;
|
||||||
nextEmptyRounds = 0;
|
static dispatch_once_t onceToken;
|
||||||
} else {
|
dispatch_once(&onceToken, ^{
|
||||||
batch = kKBBackspaceClearBatchSize;
|
// stopBoundary: 遇到这些符号就停(不删除它)
|
||||||
nextEmptyRounds = emptyRounds + 1;
|
// - 句末符号:. ! ? 。!?
|
||||||
}
|
// - 省略号:…(中文里“……”常用作句/段落的停顿)
|
||||||
if (batch <= 0) { batch = 1; }
|
// - 换行:\n \r(段落边界,避免一次“清空”跨段把全文删完)
|
||||||
|
stopBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?。!?…\n\r\u2028\u2029"];
|
||||||
|
|
||||||
if (guard >= kKBBackspaceClearMaxDeletes ||
|
// trailingBoundary: 允许作为“尾部句末符号”先删掉,再继续删上一句(更接近微信体验)
|
||||||
nextEmptyRounds > kKBBackspaceClearEmptyContextMaxRounds) {
|
// 注意:不要把换行/省略号放进来,否则可能跨段/跨停顿继续删。
|
||||||
|
trailingBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?。!?"];
|
||||||
|
|
||||||
|
// trailingWhitespace: 只跳过空格/Tab(不包含换行,换行由 stopBoundarySet 处理)
|
||||||
|
trailingWhitespaceSet = [NSCharacterSet whitespaceCharacterSet];
|
||||||
|
});
|
||||||
|
KBClearPhase phase = self.backspaceClearPhase;
|
||||||
|
|
||||||
|
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;
|
||||||
|
// 宿主(微信/QQ 等)可能在长文本场景下返回空 context,即使还有很多内容。
|
||||||
|
// 为了避免一次“清空”误删全文:一旦拿不到 before,就立刻停止本次清空。
|
||||||
|
shouldStop = YES;
|
||||||
|
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:trailingWhitespaceSet].location != NSNotFound);
|
||||||
|
BOOL isStopBoundary = ([lastChar rangeOfCharacterFromSet:stopBoundarySet].location != NSNotFound);
|
||||||
|
BOOL isTrailingBoundary = ([lastChar rangeOfCharacterFromSet:trailingBoundarySet].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 (isTrailingBoundary) {
|
||||||
|
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1];
|
||||||
|
[[KBInputBufferManager shared] applyClearDeleteCount:1];
|
||||||
|
deletedThisTick += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
phase = KBClearPhaseDeleteUntilBoundary;
|
||||||
|
}
|
||||||
|
|
||||||
|
// phase == DeleteUntilBoundary
|
||||||
|
if (isStopBoundary) {
|
||||||
|
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;
|
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;
|
__weak typeof(self) weakSelf = self;
|
||||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
|
||||||
(int64_t)(kKBBackspaceClearBatchInterval * NSEC_PER_SEC)),
|
(int64_t)(kKBBackspaceClearBatchInterval * NSEC_PER_SEC)),
|
||||||
@@ -513,7 +646,6 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)kb_capturePendingClearSnapshotIfNeeded {
|
- (void)kb_capturePendingClearSnapshotIfNeeded {
|
||||||
if ([KBBackspaceUndoManager shared].hasUndo) { return; }
|
|
||||||
if (self.pendingClearBefore.length > 0 || self.pendingClearAfter.length > 0) { return; }
|
if (self.pendingClearBefore.length > 0 || self.pendingClearAfter.length > 0) { return; }
|
||||||
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||||
UIInputViewController *ivc = KBFindInputViewController(start);
|
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||||
@@ -521,6 +653,10 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
|||||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
self.pendingClearBefore = proxy.documentContextBeforeInput ?: @"";
|
self.pendingClearBefore = proxy.documentContextBeforeInput ?: @"";
|
||||||
self.pendingClearAfter = proxy.documentContextAfterInput ?: @"";
|
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
|
@end
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ extern NSNotificationName const KBBackspaceUndoStateDidChangeNotification;
|
|||||||
/// 记录一次“立刻清空”删除的内容(基于 documentContextBeforeInput/AfterInput)。
|
/// 记录一次“立刻清空”删除的内容(基于 documentContextBeforeInput/AfterInput)。
|
||||||
- (void)recordClearWithContextBefore:(NSString *)before after:(NSString *)after;
|
- (void)recordClearWithContextBefore:(NSString *)before after:(NSString *)after;
|
||||||
|
|
||||||
|
/// 记录本次将被 deleteBackward 的内容,并执行 deleteBackward(支持多次累计,撤销时一次性插回)。
|
||||||
|
- (void)captureAndDeleteBackwardFromProxy:(id<UITextDocumentProxy>)proxy count:(NSUInteger)count;
|
||||||
|
|
||||||
/// 在指定 responder 处执行撤销(向光标处插回删除的内容)
|
/// 在指定 responder 处执行撤销(向光标处插回删除的内容)
|
||||||
- (void)performUndoFromResponder:(UIResponder *)responder;
|
- (void)performUndoFromResponder:(UIResponder *)responder;
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,38 @@
|
|||||||
|
|
||||||
#import "KBBackspaceUndoManager.h"
|
#import "KBBackspaceUndoManager.h"
|
||||||
#import "KBResponderUtils.h"
|
#import "KBResponderUtils.h"
|
||||||
|
#import "KBInputBufferManager.h"
|
||||||
|
|
||||||
NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspaceUndoStateDidChangeNotification";
|
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 ()
|
@interface KBBackspaceUndoManager ()
|
||||||
@property (nonatomic, copy) NSString *undoText;
|
@property (nonatomic, copy) NSString *undoText;
|
||||||
@property (nonatomic, assign) NSInteger undoAfterLength;
|
@property (nonatomic, assign) NSInteger undoAfterLength;
|
||||||
@property (nonatomic, assign) BOOL hasUndo;
|
@property (nonatomic, assign) BOOL hasUndo;
|
||||||
|
@property (nonatomic, assign) KBUndoSnapshotSource snapshotSource;
|
||||||
|
@property (nonatomic, strong) NSMutableArray<NSString *> *undoDeletedPieces;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation KBBackspaceUndoManager
|
@implementation KBBackspaceUndoManager
|
||||||
@@ -29,55 +54,189 @@ NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspa
|
|||||||
if (self = [super init]) {
|
if (self = [super init]) {
|
||||||
_undoText = @"";
|
_undoText = @"";
|
||||||
_undoAfterLength = 0;
|
_undoAfterLength = 0;
|
||||||
|
_snapshotSource = KBUndoSnapshotSourceNone;
|
||||||
|
_undoDeletedPieces = [NSMutableArray array];
|
||||||
}
|
}
|
||||||
return self;
|
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 {
|
- (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 *safeBefore = before ?: @"";
|
||||||
NSString *safeAfter = after ?: @"";
|
NSString *safeAfter = after ?: @"";
|
||||||
NSString *full = [safeBefore stringByAppendingString:safeAfter];
|
NSString *full = [safeBefore stringByAppendingString:safeAfter];
|
||||||
if (full.length == 0) { return; }
|
if (full.length == 0) { return; }
|
||||||
self.undoText = full;
|
self.undoText = full;
|
||||||
self.undoAfterLength = (NSInteger)safeAfter.length;
|
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 {
|
- (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 *safeBefore = before ?: @"";
|
||||||
NSString *safeAfter = after ?: @"";
|
NSString *safeAfter = after ?: @"";
|
||||||
NSString *full = [safeBefore stringByAppendingString:safeAfter];
|
NSString *contextText = [[safeBefore stringByAppendingString:safeAfter] copy];
|
||||||
if (full.length > 0) {
|
|
||||||
self.undoText = full;
|
NSString *candidate = (fallbackText.length > 0) ? fallbackText : contextText;
|
||||||
self.undoAfterLength = (NSInteger)safeAfter.length;
|
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];
|
[self kb_updateHasUndo:YES];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)performUndoFromResponder:(UIResponder *)responder {
|
- (void)performUndoFromResponder:(UIResponder *)responder {
|
||||||
if (self.undoText.length == 0) { return; }
|
if (!self.hasUndo) { return; }
|
||||||
UIInputViewController *ivc = KBFindInputViewController(responder);
|
UIInputViewController *ivc = KBFindInputViewController(responder);
|
||||||
if (!ivc) { return; }
|
if (!ivc) { return; }
|
||||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
|
NSString *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];
|
[self kb_clearAllTextForProxy:proxy];
|
||||||
[proxy insertText:self.undoText];
|
[proxy insertText:self.undoText];
|
||||||
if (self.undoAfterLength > 0 &&
|
if (self.undoAfterLength > 0 &&
|
||||||
[proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) {
|
[proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) {
|
||||||
[proxy adjustTextPositionByCharacterOffset:-self.undoAfterLength];
|
[proxy adjustTextPositionByCharacterOffset:-self.undoAfterLength];
|
||||||
}
|
}
|
||||||
|
[[KBInputBufferManager shared] resetWithText:self.undoText];
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.undoText = @"";
|
self.undoText = @"";
|
||||||
self.undoAfterLength = 0;
|
self.undoAfterLength = 0;
|
||||||
|
self.snapshotSource = KBUndoSnapshotSourceNone;
|
||||||
|
[self.undoDeletedPieces removeAllObjects];
|
||||||
[self kb_updateHasUndo:NO];
|
[self kb_updateHasUndo:NO];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)registerNonClearAction {
|
- (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.undoText = @"";
|
||||||
self.undoAfterLength = 0;
|
self.undoAfterLength = 0;
|
||||||
|
self.snapshotSource = KBUndoSnapshotSourceNone;
|
||||||
|
[self.undoDeletedPieces removeAllObjects];
|
||||||
[self kb_updateHasUndo:NO];
|
[self kb_updateHasUndo:NO];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +248,29 @@ NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspa
|
|||||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBBackspaceUndoStateDidChangeNotification object:self];
|
[[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;
|
static const NSInteger kKBUndoClearMaxRounds = 200;
|
||||||
|
|
||||||
- (void)kb_clearAllTextForProxy:(id<UITextDocumentProxy>)proxy {
|
- (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 "KBBizCode.h"
|
||||||
#import "KBBackspaceLongPressHandler.h"
|
#import "KBBackspaceLongPressHandler.h"
|
||||||
#import "KBBackspaceUndoManager.h"
|
#import "KBBackspaceUndoManager.h"
|
||||||
|
#import "KBInputBufferManager.h"
|
||||||
|
|
||||||
@interface KBFunctionView () <KBFunctionBarViewDelegate, KBStreamOverlayViewDelegate, KBFunctionTagListViewDelegate>
|
@interface KBFunctionView () <KBFunctionBarViewDelegate, KBStreamOverlayViewDelegate, KBFunctionTagListViewDelegate>
|
||||||
// UI
|
// UI
|
||||||
@@ -721,6 +722,8 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C
|
|||||||
// - 无论允许/拒绝,都把本次 changeCount 记为已处理,避免一直重复询问。
|
// - 无论允许/拒绝,都把本次 changeCount 记为已处理,避免一直重复询问。
|
||||||
|
|
||||||
- (void)startPasteboardMonitor {
|
- (void)startPasteboardMonitor {
|
||||||
|
// 禁用自动读取剪贴板,避免触发系统“允许粘贴”弹窗
|
||||||
|
return;
|
||||||
// 未开启“完全访问”时不做自动读取,避免宿主/系统拒绝并刷错误日志
|
// 未开启“完全访问”时不做自动读取,避免宿主/系统拒绝并刷错误日志
|
||||||
if (![[KBFullAccessManager shared] hasFullAccess]) return;
|
if (![[KBFullAccessManager shared] hasFullAccess]) return;
|
||||||
if (self.pasteboardTimer) return;
|
if (self.pasteboardTimer) return;
|
||||||
@@ -775,9 +778,11 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C
|
|||||||
NSLog(@"点击:删除");
|
NSLog(@"点击:删除");
|
||||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
[[KBBackspaceUndoManager shared] recordDeletionSnapshotBefore:proxy.documentContextBeforeInput
|
[[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy];
|
||||||
|
[[KBInputBufferManager shared] prepareSnapshotForDeleteWithContextBefore:proxy.documentContextBeforeInput
|
||||||
after:proxy.documentContextAfterInput];
|
after:proxy.documentContextAfterInput];
|
||||||
[proxy deleteBackward];
|
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1];
|
||||||
|
[[KBInputBufferManager shared] applyHoldDeleteCount:1];
|
||||||
}
|
}
|
||||||
- (void)onTapClear {
|
- (void)onTapClear {
|
||||||
NSLog(@"点击:清空");
|
NSLog(@"点击:清空");
|
||||||
@@ -791,6 +796,7 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C
|
|||||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||||
[proxy insertText:@"\n"];
|
[proxy insertText:@"\n"];
|
||||||
|
[[KBInputBufferManager shared] appendText:@"\n"];
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Lazy
|
#pragma mark - Lazy
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
#import "KBStreamTextView.h"
|
#import "KBStreamTextView.h"
|
||||||
#import "KBResponderUtils.h" // 通过响应链找到 UIInputViewController,并将文本输出到宿主
|
#import "KBResponderUtils.h" // 通过响应链找到 UIInputViewController,并将文本输出到宿主
|
||||||
|
#import "KBInputBufferManager.h"
|
||||||
|
|
||||||
@interface KBStreamTextView ()
|
@interface KBStreamTextView ()
|
||||||
|
|
||||||
@@ -371,6 +372,7 @@ static inline NSString *KBTrimRight(NSString *s) {
|
|||||||
if (rawText.length > 0) {
|
if (rawText.length > 0) {
|
||||||
[proxy insertText:rawText];
|
[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 */; };
|
A1B2C4212EB4B7A100000001 /* KBKeyboardPermissionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C4222EB4B7A100000001 /* KBKeyboardPermissionManager.m */; };
|
||||||
A1B2C9032FBD000100000001 /* KBBackspaceLongPressHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C9022FBD000100000001 /* KBBackspaceLongPressHandler.m */; };
|
A1B2C9032FBD000100000001 /* KBBackspaceLongPressHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C9022FBD000100000001 /* KBBackspaceLongPressHandler.m */; };
|
||||||
A1B2C9052FBD000200000001 /* KBBackspaceUndoManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C9042FBD000200000001 /* KBBackspaceUndoManager.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 */; };
|
A1B2D7022EB8C00100000001 /* KBLangTestVC.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2D7012EB8C00100000001 /* KBLangTestVC.m */; };
|
||||||
A1B2E1012EBC7AAA00000001 /* KBTopThreeView.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2E0022EBC7AAA00000001 /* KBTopThreeView.m */; };
|
A1B2E1012EBC7AAA00000001 /* KBTopThreeView.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2E0022EBC7AAA00000001 /* KBTopThreeView.m */; };
|
||||||
A1B2E1022EBC7AAA00000001 /* HomeHotCell.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2E0042EBC7AAA00000001 /* HomeHotCell.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
A1B2E0012EBC7AAA00000001 /* KBTopThreeView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBTopThreeView.h; sourceTree = "<group>"; };
|
||||||
@@ -841,6 +844,8 @@
|
|||||||
A1B2C9022FBD000100000001 /* KBBackspaceLongPressHandler.m */,
|
A1B2C9022FBD000100000001 /* KBBackspaceLongPressHandler.m */,
|
||||||
A1B2C9032FBD000200000001 /* KBBackspaceUndoManager.h */,
|
A1B2C9032FBD000200000001 /* KBBackspaceUndoManager.h */,
|
||||||
A1B2C9042FBD000200000001 /* KBBackspaceUndoManager.m */,
|
A1B2C9042FBD000200000001 /* KBBackspaceUndoManager.m */,
|
||||||
|
A1B2C9072FBD000200000003 /* KBInputBufferManager.h */,
|
||||||
|
A1B2C9082FBD000200000004 /* KBInputBufferManager.m */,
|
||||||
);
|
);
|
||||||
path = Utils;
|
path = Utils;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1947,6 +1952,7 @@
|
|||||||
04FC95702EB09516007BD342 /* KBFunctionView.m in Sources */,
|
04FC95702EB09516007BD342 /* KBFunctionView.m in Sources */,
|
||||||
A1B2C9032FBD000100000001 /* KBBackspaceLongPressHandler.m in Sources */,
|
A1B2C9032FBD000100000001 /* KBBackspaceLongPressHandler.m in Sources */,
|
||||||
A1B2C9052FBD000200000001 /* KBBackspaceUndoManager.m in Sources */,
|
A1B2C9052FBD000200000001 /* KBBackspaceUndoManager.m in Sources */,
|
||||||
|
A1B2C9092FBD000200000005 /* KBInputBufferManager.m in Sources */,
|
||||||
049FB23F2EC4B6EF00FAB05D /* KBULBridgeNotification.m in Sources */,
|
049FB23F2EC4B6EF00FAB05D /* KBULBridgeNotification.m in Sources */,
|
||||||
04791F992ED49CE7004E8522 /* KBFont.m in Sources */,
|
04791F992ED49CE7004E8522 /* KBFont.m in Sources */,
|
||||||
04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */,
|
04FC956D2EB054B7007BD342 /* KBKeyboardView.m in Sources */,
|
||||||
|
|||||||
@@ -17,12 +17,12 @@
|
|||||||
|
|
||||||
- (NSString *)coinsDisplayText {
|
- (NSString *)coinsDisplayText {
|
||||||
NSString *name = self.name ?: @"";
|
NSString *name = self.name ?: @"";
|
||||||
NSString *unit = self.unit ?: @"";
|
// NSString *unit = self.unit ?: @"";
|
||||||
if (name.length && unit.length) {
|
if (name.length) {
|
||||||
return [NSString stringWithFormat:@"%@ %@", name, unit];
|
return [NSString stringWithFormat:@"%@", name];
|
||||||
}
|
}
|
||||||
if (name.length) { return name; }
|
if (name.length) { return name; }
|
||||||
if (unit.length) { return unit; }
|
// if (unit.length) { return unit; }
|
||||||
return @"";
|
return @"";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user