我最近最初发布了一个类似的问题(感谢 OP 从那里指向这个问题)。
这个问题从来没有真正让我满意,但我确实有一个解决我原来的问题的方法,我相信这也适用于这个问题。
我的解决方案不是使用委托方法,而是覆盖NSTextView
. 所有的修改都是通过覆盖insertText:
和replaceCharactersInRange:withString:
我的insertText:
覆盖检查要插入的文本,并决定是插入未修改的文本,还是在插入之前进行其他更改。insertText:
在任何情况下,都会调用super来进行实际插入。此外,我insertText:
的它自己的撤消分组,基本上是通过beginUndoGrouping:
在插入文本之前和endUndoGrouping:
之后调用。这听起来太简单了,但对我来说似乎很管用。结果是每个插入的字符都有一个撤消操作(这是多少“真正的”文本编辑器工作 - 例如,请参见 TextMate)。此外,这使得额外的编程修改与触发它们的操作具有原子性。例如,如果用户键入 {,并且我的insertText:
以编程方式插入},两者都包含在同一个撤消分组中,因此一个撤消撤消两者。我的insertText:
样子是这样的:
- (void) insertText:(id)insertString
{
if( insertingText ) {
[super insertText:insertString];
return;
}
// We setup undo for basically every character, except for stuff we insert.
// So, start grouping.
[[self undoManager] beginUndoGrouping];
insertingText = YES;
BOOL insertedText = NO;
NSRange selection = [self selectedRange];
if( selection.length > 0 ) {
insertedText = [self didHandleInsertOfString:insertString withSelection:selection];
}
else {
insertedText = [self didHandleInsertOfString:insertString];
}
if( !insertedText ) {
[super insertText:insertString];
}
insertingText = NO;
// End undo grouping.
[[self undoManager] endUndoGrouping];
}
insertingText
是我用来跟踪是否插入文本的 ivar。didHandleInsertOfString:
并且didHandleInsertOfString:withSelection:
是实际上最终执行insertText:
修改内容的调用的函数。它们都很长,但我会在最后包含一个示例。
我只是覆盖replaceCharactersInRange:withString:
,因为我有时使用该调用来修改文本,它绕过了撤消。但是,您可以通过调用将其连接到撤消shouldChangeTextInRange:replacementString:
。所以我的覆盖就是这样做的。
// We call replaceChractersInRange all over the place, and that does an end-run
// around Undo, unless you first call shouldChangeTextInRange:withString (it does
// the Undo stuff). Rather than sprinkle those all over the place, do it once
// here.
- (void) replaceCharactersInRange:(NSRange)range withString:(NSString*)aString
{
if( [self shouldChangeTextInRange:range replacementString:aString] ) {
[super replaceCharactersInRange:range withString:aString];
}
}
didHandleInsertOfString:
做了一大堆东西,但它的要点是它要么插入文本(通过insertText:
或replaceCharactersInRange:withString:
),如果它做了任何插入则返回 YES,如果它没有插入则返回 NO。它看起来像这样:
- (BOOL) didHandleInsertOfString:(NSString*)string
{
if( [string length] == 0 ) return NO;
unichar character = [string characterAtIndex:0];
if( character == '(' || character == '[' || character == '{' || character == '\"' )
{
// (, [, {, ", ` : insert that, and end character.
unichar startCharacter = character;
unichar endCharacter;
switch( startCharacter ) {
case '(': endCharacter = ')'; break;
case '[': endCharacter = ']'; break;
case '{': endCharacter = '}'; break;
case '\"': endCharacter = '\"'; break;
}
if( character == '\"' ) {
// Double special case for quote. If the character immediately to the right
// of the insertion point is a number, we're done. That way if you type,
// say, 27", it works as you expect.
NSRange selectionRange = [self selectedRange];
if( selectionRange.location > 0 ) {
unichar lastChar = [[self string] characterAtIndex:selectionRange.location - 1];
if( [[NSCharacterSet decimalDigitCharacterSet] characterIsMember:lastChar] ) {
return NO;
}
}
// Special case for quote, if we autoinserted that.
// Type through it and we're done.
if( lastCharacterInserted == '\"' ) {
lastCharacterInserted = 0;
lastCharacterWhichCausedInsertion = 0;
[self moveRight:nil];
return YES;
}
}
NSString* replacementString = [NSString stringWithFormat:@"%c%c", startCharacter, endCharacter];
[self insertText:replacementString];
[self moveLeft:nil];
// Remember the character, so if the user deletes it we remember to also delete the
// one we inserted.
lastCharacterInserted = endCharacter;
lastCharacterWhichCausedInsertion = startCharacter;
if( lastCharacterWhichCausedInsertion == '{' ) {
justInsertedBrace = YES;
}
return YES;
}
// A bunch of other cases here...
return NO;
}
我要指出,这段代码没有经过实战测试:我还没有在运输应用程序中使用过它。但这是我目前在打算在今年晚些时候发布的项目中使用的精简版代码。到目前为止,它似乎运作良好。
为了真正了解它是如何工作的,您可能需要一个示例项目,所以我在 github 上发布了一个。