Cocoa: поиск общей стратегии для программной манипуляции хранилищем NSTextView без испорчения отмены

Я пишу специальный текстовый редактор в cocoa, который выполняет такие функции, как автоматическая подмена текста, встроенные текстовые дополнения (ala Xcode) и т.д.

Мне нужно иметь возможность программно манипулировать NSTextView s NSTextStorage в ответ на 1) ввод данных пользователя, 2) вставка пользователя, 3) удаление пользователем текста.

Я пробовал два разных общих подхода, и оба они вызвали отладчик NSTextView, который был отключен от синхронизации по-разному. В каждом случае я использую только методы NSTextView delegate. Я пытался избежать подклассификации NSTextView или NSTextStorage (хотя я буду подклассифицировать, если необходимо).

Первый подход, который я пробовал, выполнял манипуляции из метода textView delegate s textDidChange. Из этого метода я проанализировал, что было изменено в textView, а затем вызвал метод общего назначения для модификации текста, который обернул изменения в textStorage вызовами shouldChangeTextInRange: и didChangeText:. Некоторые из программных изменений допускали чистые отмены, но некоторые не делали.

Второй (и, возможно, более интуитивно понятный, поскольку он вносит изменения до того, как текст действительно появляется в подходе textView), который я пытался, делал манипуляции из метода delegate s shouldChangeTextInRange:, снова используя ту же общую цель способ хранения, который переносит изменения в хранилище с вызовом shouldChangeTextInRange: и didChangeText:. Поскольку эти изменения запускались изначально из shouldChangeTextInRange:, я устанавливал флаг, который указывал, что внутренний вызов shouldChangeTextInRange: игнорируется, чтобы не вводить рекурсивную черноту. Опять же, некоторые из программных изменений допускали чистые отмены, но некоторые из них (хотя и разные на этот раз и по-разному).

Со всем этим фоном, на мой вопрос, может ли кто-нибудь указать мне общую стратегию для программного управления хранилищем NSTextView, которая будет содержать диспетчер отмены в чистоте и в синхронизации?

В каком методе делегата NSTextView следует обратить внимание на текстовые изменения в textView (путем ввода, вставки или удаления) и выполнить манипуляции с NSTextStorage? Или это единственный способ сделать это путем подклассификации либо NSTextView, либо NSTextStorage?

Ответы

Ответ 1

Я изначально разместил аналогичный вопрос довольно недавно (спасибо OP за то, что он указал туда обратно на этот вопрос).

Этот вопрос никогда не отвечал на мое удовлетворение, но у меня есть решение моей исходной проблемы, которая, как мне кажется, также относится к этому.

Мое решение не используется для методов делегата, а скорее для переопределения NSTextView. Все модификации выполняются путем переопределения insertText: и replaceCharactersInRange:withString:

My insertText: override проверяет текст, который нужно вставить, и решает, следует ли вставлять это неизмененное или делать другие изменения перед его вставкой. В любом случае супер insertText: вызывается для фактической вставки. Кроме того, мой insertText: имеет свою собственную группу отмены, в основном, вызывая beginUndoGrouping: перед вставкой текста и endUndoGrouping: после. Это звучит слишком просто для работы, но, похоже, он отлично работает для меня. В результате вы получаете одну операцию отмены для вставленного символа (что такое количество "реальных" текстовых редакторов) - см., Например, TextMate). Кроме того, это делает дополнительные программные модификации атомарными с операцией, которая их запускает. Например, если пользователь вводит {и my 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: делает весь материал buncha, но суть в том, что он либо вставляет текст (через 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.

Ответ 2

Правильно, это далеко не идеальное решение, но это решение рода.

Текстовое хранилище обновляет менеджер отмены, основанный на "группах". Эти группы группируют ряд изменений (которые я не могу полностью вспомнить о моей голове), но я помню, что при изменении выбора создается новый.

Это приводит к возможному решению быстро изменить выбор на что-то еще, а затем вернуть его обратно. Не идеальное решение, но этого может быть достаточно, чтобы заставить текстовое хранилище нажать новое состояние на менеджер отмены.

Я возьму немного больше взгляда и расследования и посмотрю, не могу ли я найти/проследить, что происходит.

edit: Я должен, вероятно, упомянуть, что прошло некоторое время, так как я использовал NSTextView и в настоящее время не имею доступа к Xcode на этом компьютере, чтобы убедиться, что это работает. Надеюсь, он это сделает.