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

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

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

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

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

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

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

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


person pjv    schedule 07.04.2011    source источник
comment
Мое приложение выполняет большинство своих манипуляций в -textStorageWillProcessEditing: NSTextStorageDelegate, но ему нужно манипулировать только атрибутами, а не символами. Тем не менее, это может быть еще одна вещь, которую вы можете попробовать.   -  person Becca Royal-Gordon    schedule 24.05.2013
comment
Пожалуйста, уточните те методы изменения хранилища общего назначения, которые оборачивают изменения в хранилище вызовом shouldChangeTextInRange: и didChangeText:. Делать что-то внутри метода делегата, который рекурсивно вызывает один и тот же метод делегата, звучит подозрительно.   -  person Pierre Houston    schedule 25.05.2013
comment
@smallduck: Прошло примерно два года с тех пор, как я изначально задал этот вопрос, и я отказался от проекта - в значительной степени из-за того, что не смог удовлетворительно решить эту проблему отмены - я не могу вспомнить многих деталей. Я потратил много времени на разработку, пытаясь разобраться с этим, и пробовал много других подходов, кроме того, что я написал выше. Я знаю, что это возможно, потому что это делает редактор Xcode, но я так и не нашел способа программно изменить текст, не выводя диспетчер отмены из синхронизации.   -  person pjv    schedule 25.05.2013
comment
Вы можете взглянуть на IDEKit, прежде чем сдаваться. быстро. Большинство из них настраиваемые, но основной класс редактора по-прежнему является подклассом NSTextView.   -  person CodaFi    schedule 27.05.2013


Ответы (2)


Первоначально я опубликовал похожий вопрос сравнительно недавно (спасибо в 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
comment
Вот это да. Большое спасибо за такой щедрый и подробный ответ! Общий метод, который вы описали - перенос вашего набора изменений в beginUndoGrouping: и endUndoGrouping - это именно то, что, как я надеялся, будет работать в методах делегата NSTextview. Логически кажется, что так и должно быть, но очевидно, что там происходит что-то непрозрачное (я знаю... Apple? Непрозрачное? Никогда!). В любом случае, один только этот ответ стоит награды. Пример github ставит это выше всяких похвал. СПАСИБО! - person pjv; 26.05.2013
comment
Эй, большое спасибо за этот ответ! Я потратил впустую целый день и большую часть ночи, пытаясь сделать то же самое в shouldChangeTextIn:, постоянно не в состоянии заставить UndoManager заботиться о моих изменениях. Я уже шел спать, когда мне пришла в голову идея попробовать вместо этого вставить текст, и я почувствовал такое облегчение, мгновенно найдя ваш ответ! - person Stefan Stuckmann; 01.09.2020
comment
это именно то, что мне было нужно - спасибо! Я также смог удалить вызовы beginUndoGrouping и endUndoGrouping в insertText. в противном случае я бы получал шаги отмены для каждого нажатия клавиши. как только я удалил это, любые пользовательские изменения, которые я внес в текст внутри didHandleInsertOfString, работали отлично и работали с отменой / повтором в самый раз. Спасибо! - person adam.wulf; 19.04.2021

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

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

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

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

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

person Tom Hancocks    schedule 25.05.2013
comment
Я не могу сказать, что следую вашей концепции, не видя кода или псевдокода, но я знаю о группах изменений. IIRC, я пытался управлять особенностями групп отмены, используя вызовы beginUndoGrouping и endUndoGrouping (или что-то в этом роде). Из документации по диспетчеру отмены я понял, что упаковка программных наборов изменений в эти вызовы должна была быть всем, что было необходимо для правильного управления стеком изменений, но я не мог заставить его работать на меня. - person pjv; 25.05.2013