处理键盘长按删除 撤销出现的bug
This commit is contained in:
@@ -363,30 +363,33 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
||||
|
||||
// MARK: - KBKeyBoardMainViewDelegate
|
||||
- (void)keyBoardMainView:(KBKeyBoardMainView *)keyBoardMainView didTapKey:(KBKey *)key {
|
||||
if (key.type != KBKeyTypeShift && key.type != KBKeyTypeModeChange) {
|
||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||
}
|
||||
switch (key.type) {
|
||||
case KBKeyTypeCharacter: {
|
||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||
NSString *text = key.output ?: key.title ?: @"";
|
||||
[self.textDocumentProxy insertText:text];
|
||||
[self kb_updateCurrentWordWithInsertedText:text];
|
||||
} break;
|
||||
case KBKeyTypeBackspace:
|
||||
[[KBBackspaceUndoManager shared] recordDeletionSnapshotBefore:self.textDocumentProxy.documentContextBeforeInput
|
||||
after:self.textDocumentProxy.documentContextAfterInput];
|
||||
[self.textDocumentProxy deleteBackward];
|
||||
[self kb_scheduleContextRefreshResetSuppression:NO];
|
||||
break;
|
||||
case KBKeyTypeSpace:
|
||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||
[self.textDocumentProxy insertText:@" "];
|
||||
[self kb_clearCurrentWord];
|
||||
break;
|
||||
case KBKeyTypeReturn:
|
||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||
[self.textDocumentProxy insertText:@"\n"];
|
||||
[self kb_clearCurrentWord];
|
||||
break;
|
||||
case KBKeyTypeGlobe:
|
||||
[self advanceToNextInputMode]; break;
|
||||
case KBKeyTypeCustom:
|
||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||
// 点击自定义键切换到功能面板
|
||||
[self showFunctionPanel:YES];
|
||||
[self kb_clearCurrentWord];
|
||||
|
||||
@@ -48,6 +48,8 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
@property (nonatomic, assign) CGPoint backspaceLastTouchPointInSelf;
|
||||
@property (nonatomic, assign) NSUInteger backspaceClearToken;
|
||||
@property (nonatomic, strong) UILabel *backspaceClearLabel;
|
||||
@property (nonatomic, copy) NSString *pendingClearBefore;
|
||||
@property (nonatomic, copy) NSString *pendingClearAfter;
|
||||
@end
|
||||
|
||||
@implementation KBBackspaceLongPressHandler
|
||||
@@ -73,6 +75,8 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
self.backspaceHasLastTouchPoint = NO;
|
||||
self.backspaceHoldToken += 1;
|
||||
[self kb_hideBackspaceClearLabel];
|
||||
self.pendingClearBefore = nil;
|
||||
self.pendingClearAfter = nil;
|
||||
|
||||
if (!button) { return; }
|
||||
|
||||
@@ -99,7 +103,10 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
}
|
||||
switch (gr.state) {
|
||||
case UIGestureRecognizerStateBegan: {
|
||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||
[self kb_captureDeletionSnapshotIfNeeded];
|
||||
if (self.showClearLabelEnabled) {
|
||||
[self kb_capturePendingClearSnapshotIfNeeded];
|
||||
}
|
||||
self.backspaceHoldToken += 1;
|
||||
NSUInteger token = self.backspaceHoldToken;
|
||||
self.backspaceHoldActive = YES;
|
||||
@@ -310,6 +317,9 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
[self kb_hideBackspaceClearLabel];
|
||||
if (shouldClear) {
|
||||
[self kb_clearAllInput];
|
||||
} else {
|
||||
self.pendingClearBefore = nil;
|
||||
self.pendingClearAfter = nil;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,9 +431,12 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||
if (ivc) {
|
||||
NSString *before = ivc.textDocumentProxy.documentContextBeforeInput ?: @"";
|
||||
[[KBBackspaceUndoManager shared] recordClearWithContext:before];
|
||||
NSString *before = self.pendingClearBefore ?: (ivc.textDocumentProxy.documentContextBeforeInput ?: @"");
|
||||
NSString *after = self.pendingClearAfter ?: (ivc.textDocumentProxy.documentContextAfterInput ?: @"");
|
||||
[[KBBackspaceUndoManager shared] recordClearWithContextBefore:before after:after];
|
||||
}
|
||||
self.pendingClearBefore = nil;
|
||||
self.pendingClearAfter = nil;
|
||||
self.backspaceClearToken += 1;
|
||||
NSUInteger token = self.backspaceClearToken;
|
||||
[self kb_clearAllInputStepForToken:token guard:0 emptyRounds:0];
|
||||
@@ -489,4 +502,25 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
return self.backspaceButton.superview;
|
||||
}
|
||||
|
||||
- (void)kb_captureDeletionSnapshotIfNeeded {
|
||||
if ([KBBackspaceUndoManager shared].hasUndo) { return; }
|
||||
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||
if (!ivc) { return; }
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
[[KBBackspaceUndoManager shared] recordDeletionSnapshotBefore:proxy.documentContextBeforeInput
|
||||
after:proxy.documentContextAfterInput];
|
||||
}
|
||||
|
||||
- (void)kb_capturePendingClearSnapshotIfNeeded {
|
||||
if ([KBBackspaceUndoManager shared].hasUndo) { return; }
|
||||
if (self.pendingClearBefore.length > 0 || self.pendingClearAfter.length > 0) { return; }
|
||||
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||
if (!ivc) { return; }
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
self.pendingClearBefore = proxy.documentContextBeforeInput ?: @"";
|
||||
self.pendingClearAfter = proxy.documentContextAfterInput ?: @"";
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -15,13 +15,16 @@ extern NSNotificationName const KBBackspaceUndoStateDidChangeNotification;
|
||||
|
||||
+ (instancetype)shared;
|
||||
|
||||
/// 记录一次“立刻清空”删除的内容(基于 documentContextBeforeInput)
|
||||
- (void)recordClearWithContext:(NSString *)context;
|
||||
/// 记录一次删除前的快照(不改变撤销按钮显示)。
|
||||
- (void)recordDeletionSnapshotBefore:(NSString *)before after:(NSString *)after;
|
||||
|
||||
/// 记录一次“立刻清空”删除的内容(基于 documentContextBeforeInput/AfterInput)。
|
||||
- (void)recordClearWithContextBefore:(NSString *)before after:(NSString *)after;
|
||||
|
||||
/// 在指定 responder 处执行撤销(向光标处插回删除的内容)
|
||||
- (void)performUndoFromResponder:(UIResponder *)responder;
|
||||
|
||||
/// 非清空行为触发时,清理撤销状态
|
||||
/// 非删除行为触发时,清理撤销状态
|
||||
- (void)registerNonClearAction;
|
||||
|
||||
@end
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspaceUndoStateDidChangeNotification";
|
||||
|
||||
@interface KBBackspaceUndoManager ()
|
||||
@property (nonatomic, strong) NSMutableArray<NSString *> *segments; // deletion order (last -> first)
|
||||
@property (nonatomic, assign) BOOL lastActionWasClear;
|
||||
@property (nonatomic, copy) NSString *undoText;
|
||||
@property (nonatomic, assign) NSInteger undoAfterLength;
|
||||
@property (nonatomic, assign) BOOL hasUndo;
|
||||
@end
|
||||
|
||||
@@ -27,42 +27,57 @@ NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspa
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
_segments = [NSMutableArray array];
|
||||
_undoText = @"";
|
||||
_undoAfterLength = 0;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)recordClearWithContext:(NSString *)context {
|
||||
if (context.length == 0) { return; }
|
||||
NSString *segment = [self kb_segmentForClearFromContext:context];
|
||||
if (segment.length == 0) { return; }
|
||||
|
||||
if (!self.lastActionWasClear) {
|
||||
[self.segments removeAllObjects];
|
||||
- (void)recordDeletionSnapshotBefore:(NSString *)before after:(NSString *)after {
|
||||
if (self.undoText.length > 0) { return; }
|
||||
NSString *safeBefore = before ?: @"";
|
||||
NSString *safeAfter = after ?: @"";
|
||||
NSString *full = [safeBefore stringByAppendingString:safeAfter];
|
||||
if (full.length == 0) { return; }
|
||||
self.undoText = full;
|
||||
self.undoAfterLength = (NSInteger)safeAfter.length;
|
||||
}
|
||||
[self.segments addObject:segment];
|
||||
self.lastActionWasClear = YES;
|
||||
|
||||
- (void)recordClearWithContextBefore:(NSString *)before after:(NSString *)after {
|
||||
if (self.undoText.length == 0) {
|
||||
NSString *safeBefore = before ?: @"";
|
||||
NSString *safeAfter = after ?: @"";
|
||||
NSString *full = [safeBefore stringByAppendingString:safeAfter];
|
||||
if (full.length > 0) {
|
||||
self.undoText = full;
|
||||
self.undoAfterLength = (NSInteger)safeAfter.length;
|
||||
}
|
||||
}
|
||||
if (self.undoText.length == 0) { return; }
|
||||
[self kb_updateHasUndo:YES];
|
||||
}
|
||||
|
||||
- (void)performUndoFromResponder:(UIResponder *)responder {
|
||||
if (self.segments.count == 0) { return; }
|
||||
if (self.undoText.length == 0) { return; }
|
||||
UIInputViewController *ivc = KBFindInputViewController(responder);
|
||||
if (!ivc) { return; }
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
NSString *text = [self kb_buildUndoText];
|
||||
if (text.length == 0) { return; }
|
||||
[proxy insertText:text];
|
||||
[self kb_clearAllTextForProxy:proxy];
|
||||
[proxy insertText:self.undoText];
|
||||
if (self.undoAfterLength > 0 &&
|
||||
[proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) {
|
||||
[proxy adjustTextPositionByCharacterOffset:-self.undoAfterLength];
|
||||
}
|
||||
|
||||
[self.segments removeAllObjects];
|
||||
self.lastActionWasClear = NO;
|
||||
self.undoText = @"";
|
||||
self.undoAfterLength = 0;
|
||||
[self kb_updateHasUndo:NO];
|
||||
}
|
||||
|
||||
- (void)registerNonClearAction {
|
||||
self.lastActionWasClear = NO;
|
||||
if (self.segments.count == 0) { return; }
|
||||
[self.segments removeAllObjects];
|
||||
if (self.undoText.length == 0) { return; }
|
||||
self.undoText = @"";
|
||||
self.undoAfterLength = 0;
|
||||
[self kb_updateHasUndo:NO];
|
||||
}
|
||||
|
||||
@@ -74,97 +89,34 @@ NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspa
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBBackspaceUndoStateDidChangeNotification object:self];
|
||||
}
|
||||
|
||||
- (NSString *)kb_segmentForClearFromContext:(NSString *)context {
|
||||
NSInteger length = context.length;
|
||||
if (length == 0) { return @""; }
|
||||
static const NSInteger kKBUndoClearMaxRounds = 200;
|
||||
|
||||
static NSCharacterSet *sentenceBoundarySet = nil;
|
||||
static NSCharacterSet *whitespaceSet = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;。!?;…\n"];
|
||||
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||||
});
|
||||
- (void)kb_clearAllTextForProxy:(id<UITextDocumentProxy>)proxy {
|
||||
if (!proxy) { return; }
|
||||
|
||||
NSInteger end = length;
|
||||
while (end > 0) {
|
||||
unichar ch = [context characterAtIndex:end - 1];
|
||||
if ([whitespaceSet characterIsMember:ch]) {
|
||||
end -= 1;
|
||||
} else {
|
||||
break;
|
||||
if ([proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) {
|
||||
NSInteger guard = 0;
|
||||
NSString *contextAfter = proxy.documentContextAfterInput ?: @"";
|
||||
while (contextAfter.length > 0 && guard < kKBUndoClearMaxRounds) {
|
||||
NSInteger offset = (NSInteger)contextAfter.length;
|
||||
[proxy adjustTextPositionByCharacterOffset:offset];
|
||||
for (NSUInteger i = 0; i < contextAfter.length; i++) {
|
||||
[proxy deleteBackward];
|
||||
}
|
||||
}
|
||||
NSInteger searchEnd = end;
|
||||
while (searchEnd > 0) {
|
||||
unichar ch = [context characterAtIndex:searchEnd - 1];
|
||||
if ([sentenceBoundarySet characterIsMember:ch]) {
|
||||
searchEnd -= 1;
|
||||
} else {
|
||||
break;
|
||||
guard += 1;
|
||||
contextAfter = proxy.documentContextAfterInput ?: @"";
|
||||
}
|
||||
}
|
||||
|
||||
NSInteger boundaryIndex = NSNotFound;
|
||||
for (NSInteger i = searchEnd - 1; i >= 0; i--) {
|
||||
unichar ch = [context characterAtIndex:i];
|
||||
if ([sentenceBoundarySet characterIsMember:ch]) {
|
||||
boundaryIndex = i;
|
||||
break;
|
||||
NSInteger guard = 0;
|
||||
NSString *contextBefore = proxy.documentContextBeforeInput ?: @"";
|
||||
while (contextBefore.length > 0 && guard < kKBUndoClearMaxRounds) {
|
||||
for (NSUInteger i = 0; i < contextBefore.length; i++) {
|
||||
[proxy deleteBackward];
|
||||
}
|
||||
guard += 1;
|
||||
contextBefore = proxy.documentContextBeforeInput ?: @"";
|
||||
}
|
||||
|
||||
NSInteger start = (boundaryIndex == NSNotFound) ? 0 : (boundaryIndex + 1);
|
||||
if (start >= length) { return @""; }
|
||||
return [context substringFromIndex:start];
|
||||
}
|
||||
|
||||
- (NSString *)kb_buildUndoText {
|
||||
if (self.segments.count == 0) { return @""; }
|
||||
NSArray<NSString *> *ordered = [[self.segments reverseObjectEnumerator] allObjects];
|
||||
NSMutableString *result = [NSMutableString string];
|
||||
for (NSInteger i = 0; i < ordered.count; i++) {
|
||||
NSString *segment = ordered[i] ?: @"";
|
||||
if (segment.length == 0) { continue; }
|
||||
if (i < ordered.count - 1) {
|
||||
segment = [self kb_replaceTrailingBoundaryWithComma:segment];
|
||||
}
|
||||
[result appendString:segment];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSString *)kb_replaceTrailingBoundaryWithComma:(NSString *)segment {
|
||||
if (segment.length == 0) { return segment; }
|
||||
|
||||
static NSCharacterSet *boundarySet = nil;
|
||||
static NSCharacterSet *englishBoundarySet = nil;
|
||||
static NSCharacterSet *whitespaceSet = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
boundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;。!?;…\n"];
|
||||
englishBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;"];
|
||||
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||||
});
|
||||
|
||||
NSInteger idx = segment.length - 1;
|
||||
while (idx >= 0) {
|
||||
unichar ch = [segment characterAtIndex:idx];
|
||||
if ([whitespaceSet characterIsMember:ch]) {
|
||||
idx -= 1;
|
||||
continue;
|
||||
}
|
||||
if (![boundarySet characterIsMember:ch]) {
|
||||
return segment;
|
||||
}
|
||||
NSString *comma = [englishBoundarySet characterIsMember:ch] ? @"," : @",";
|
||||
NSMutableString *mutable = [segment mutableCopy];
|
||||
NSRange r = NSMakeRange(idx, 1);
|
||||
[mutable replaceCharactersInRange:r withString:comma];
|
||||
return mutable;
|
||||
}
|
||||
|
||||
return segment;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -770,17 +770,20 @@ static void KBULDarwinCallback(CFNotificationCenterRef center, void *observer, C
|
||||
- (void)kb_fullAccessChanged {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{ [self kb_refreshPasteboardMonitor]; });
|
||||
}
|
||||
|
||||
- (void)onTapDelete {
|
||||
NSLog(@"点击:删除");
|
||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
[[KBBackspaceUndoManager shared] recordDeletionSnapshotBefore:proxy.documentContextBeforeInput
|
||||
after:proxy.documentContextAfterInput];
|
||||
[proxy deleteBackward];
|
||||
}
|
||||
- (void)onTapClear {
|
||||
NSLog(@"点击:清空");
|
||||
[self.backspaceHandler performClearAction];
|
||||
}
|
||||
|
||||
- (void)onTapSend {
|
||||
NSLog(@"点击:发送");
|
||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#import "KBSuggestionBarView.h"
|
||||
#import "Masonry.h"
|
||||
#import "KBSkinManager.h"
|
||||
#import "KBBackspaceUndoManager.h"
|
||||
|
||||
@interface KBKeyBoardMainView ()<KBToolBarDelegate, KBKeyboardViewDelegate, KBEmojiPanelViewDelegate, KBSuggestionBarViewDelegate>
|
||||
@property (nonatomic, strong) KBToolBar *topBar;
|
||||
@@ -87,10 +88,18 @@
|
||||
make.top.equalTo(self.topBar.mas_bottom).offset(barSpacing);
|
||||
}];
|
||||
// 功能面板切换交由外部控制器处理;此处不直接创建/管理
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(kb_undoStateChanged)
|
||||
name:KBBackspaceUndoStateDidChangeNotification
|
||||
object:nil];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)setEmojiPanelVisible:(BOOL)visible animated:(BOOL)animated {
|
||||
if (self.emojiPanelVisible == visible) return;
|
||||
self.emojiPanelVisible = visible;
|
||||
@@ -108,7 +117,7 @@
|
||||
self.emojiView.alpha = visible ? 1.0 : 0.0;
|
||||
self.keyboardView.alpha = visible ? 0.0 : 1.0;
|
||||
self.topBar.alpha = visible ? 0.0 : 1.0;
|
||||
self.suggestionBar.alpha = visible ? 0.0 : (self.suggestionBarHasItems ? 1.0 : 0.0);
|
||||
self.suggestionBar.alpha = visible ? 0.0 : ([self kb_shouldShowSuggestions] ? 1.0 : 0.0);
|
||||
};
|
||||
void (^completion)(BOOL) = ^(BOOL finished) {
|
||||
self.emojiView.hidden = !visible;
|
||||
@@ -117,7 +126,7 @@
|
||||
if (visible) {
|
||||
self.suggestionBar.hidden = YES;
|
||||
} else {
|
||||
self.suggestionBar.hidden = !self.suggestionBarHasItems;
|
||||
self.suggestionBar.hidden = ![self kb_shouldShowSuggestions];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -258,14 +267,7 @@
|
||||
- (void)kb_setSuggestions:(NSArray<NSString *> *)suggestions {
|
||||
self.suggestionBarHasItems = (suggestions.count > 0);
|
||||
[self.suggestionBar updateSuggestions:suggestions];
|
||||
|
||||
if (self.emojiPanelVisible) {
|
||||
self.suggestionBar.hidden = YES;
|
||||
self.suggestionBar.alpha = 0.0;
|
||||
} else {
|
||||
self.suggestionBar.hidden = !self.suggestionBarHasItems;
|
||||
self.suggestionBar.alpha = self.suggestionBarHasItems ? 1.0 : 0.0;
|
||||
}
|
||||
[self kb_applySuggestionVisibility];
|
||||
}
|
||||
|
||||
#pragma mark - KBSuggestionBarViewDelegate
|
||||
@@ -276,4 +278,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)kb_undoStateChanged {
|
||||
[self kb_applySuggestionVisibility];
|
||||
}
|
||||
|
||||
- (BOOL)kb_shouldShowSuggestions {
|
||||
if (self.emojiPanelVisible) { return NO; }
|
||||
if (![KBBackspaceUndoManager shared].hasUndo && self.suggestionBarHasItems) {
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)kb_applySuggestionVisibility {
|
||||
BOOL shouldShow = [self kb_shouldShowSuggestions];
|
||||
self.suggestionBar.hidden = !shouldShow;
|
||||
self.suggestionBar.alpha = shouldShow ? 1.0 : 0.0;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
Reference in New Issue
Block a user