Первоначально я опубликовал похожий вопрос сравнительно недавно (спасибо в OP за указание оттуда обратно на этот вопрос).
На этот вопрос я так и не получил удовлетворительного ответа, но у меня есть решение моей первоначальной проблемы, которое, как я полагаю, применимо и к этому.
Мое решение не используется для методов делегата, а скорее для переопределения NSTextView
. Все модификации выполняются путем переопределения insertText:
и replaceCharactersInRange:withString:
.
Мое переопределение insertText:
проверяет вставляемый текст и решает, следует ли вставить его без изменений или внести другие изменения перед его вставкой. В любом случае для фактической вставки вызывается супер insertText:
. Кроме того, мой 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. .
person
zpasternack
schedule
25.05.2013
-textStorageWillProcessEditing:
NSTextStorageDelegate
, но ему нужно манипулировать только атрибутами, а не символами. Тем не менее, это может быть еще одна вещь, которую вы можете попробовать. - person Becca Royal-Gordon   schedule 24.05.2013