171 lines
5.3 KiB
Objective-C
171 lines
5.3 KiB
Objective-C
//
|
||
// KBBackspaceUndoManager.m
|
||
// CustomKeyboard
|
||
//
|
||
|
||
#import "KBBackspaceUndoManager.h"
|
||
#import "KBResponderUtils.h"
|
||
|
||
NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspaceUndoStateDidChangeNotification";
|
||
|
||
@interface KBBackspaceUndoManager ()
|
||
@property (nonatomic, strong) NSMutableArray<NSString *> *segments; // deletion order (last -> first)
|
||
@property (nonatomic, assign) BOOL lastActionWasClear;
|
||
@property (nonatomic, assign) BOOL hasUndo;
|
||
@end
|
||
|
||
@implementation KBBackspaceUndoManager
|
||
|
||
+ (instancetype)shared {
|
||
static KBBackspaceUndoManager *mgr = nil;
|
||
static dispatch_once_t onceToken;
|
||
dispatch_once(&onceToken, ^{
|
||
mgr = [[KBBackspaceUndoManager alloc] init];
|
||
});
|
||
return mgr;
|
||
}
|
||
|
||
- (instancetype)init {
|
||
if (self = [super init]) {
|
||
_segments = [NSMutableArray array];
|
||
}
|
||
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];
|
||
}
|
||
[self.segments addObject:segment];
|
||
self.lastActionWasClear = YES;
|
||
[self kb_updateHasUndo:YES];
|
||
}
|
||
|
||
- (void)performUndoFromResponder:(UIResponder *)responder {
|
||
if (self.segments.count == 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.segments removeAllObjects];
|
||
self.lastActionWasClear = NO;
|
||
[self kb_updateHasUndo:NO];
|
||
}
|
||
|
||
- (void)registerNonClearAction {
|
||
self.lastActionWasClear = NO;
|
||
if (self.segments.count == 0) { return; }
|
||
[self.segments removeAllObjects];
|
||
[self kb_updateHasUndo:NO];
|
||
}
|
||
|
||
#pragma mark - Helpers
|
||
|
||
- (void)kb_updateHasUndo:(BOOL)hasUndo {
|
||
if (self.hasUndo == hasUndo) { return; }
|
||
self.hasUndo = hasUndo;
|
||
[[NSNotificationCenter defaultCenter] postNotificationName:KBBackspaceUndoStateDidChangeNotification object:self];
|
||
}
|
||
|
||
- (NSString *)kb_segmentForClearFromContext:(NSString *)context {
|
||
NSInteger length = context.length;
|
||
if (length == 0) { return @""; }
|
||
|
||
static NSCharacterSet *sentenceBoundarySet = nil;
|
||
static NSCharacterSet *whitespaceSet = nil;
|
||
static dispatch_once_t onceToken;
|
||
dispatch_once(&onceToken, ^{
|
||
sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;。!?;…\n"];
|
||
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||
});
|
||
|
||
NSInteger end = length;
|
||
while (end > 0) {
|
||
unichar ch = [context characterAtIndex:end - 1];
|
||
if ([whitespaceSet characterIsMember:ch]) {
|
||
end -= 1;
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
NSInteger searchEnd = end;
|
||
while (searchEnd > 0) {
|
||
unichar ch = [context characterAtIndex:searchEnd - 1];
|
||
if ([sentenceBoundarySet characterIsMember:ch]) {
|
||
searchEnd -= 1;
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
|
||
NSInteger boundaryIndex = NSNotFound;
|
||
for (NSInteger i = searchEnd - 1; i >= 0; i--) {
|
||
unichar ch = [context characterAtIndex:i];
|
||
if ([sentenceBoundarySet characterIsMember:ch]) {
|
||
boundaryIndex = i;
|
||
break;
|
||
}
|
||
}
|
||
|
||
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
|