Какао: ищет общую стратегию для программных манипуляций с хранилищем NSTextView без ошибок - PullRequest
15 голосов
/ 07 апреля 2011

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

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

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

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

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

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

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

Ответы [ 2 ]

12 голосов
/ 26 мая 2013

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

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

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

Переопределение insertText: проверяет вставляемый текст и решает, вставлять ли этот неизмененный или делать другие изменения перед его вставкой. В любом случае супер 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 - это ивар, который я использую, чтобы отслеживать, вставляется текст или нет. 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 .

0 голосов
/ 25 мая 2013

Да, это ни в коем случае не идеальное решение, но это своего рода решение.

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

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

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

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

...