Cocoa: Szukam ogólnej strategii programowej manipulacji pamięcią masową NSTextView bez bałaganu

Piszę specjalny edytor tekstu w cocoa, który robi rzeczy takie jak automatyczne zastępowanie tekstu, uzupełnianie tekstu w wierszu (ala Xcode) itp.

Muszę być w stanie programowo manipulować NSTextView's NSTextStorage w odpowiedzi na 1) typowanie użytkownika, 2) wklejanie użytkownika, 3) upuszczanie tekstu.

Próbowałem dwóch różnych podejść ogólnych i oba spowodowały, że natywny menedżer cofania NSTextView przestał być synchronizowany na różne sposoby. W w każdym przypadku używam tylko metod NSTextView delegat. Starałem się unikać podklasowania NSTextview lub NSTextStorage (choć w razie potrzeby będę podklasował).

Pierwszym podejściem, które próbowałem było wykonywanie manipulacji z wewnątrz textView delegate'S textDidChange metoda. W ramach tej metody przeanalizowałem, co zostało zmienione w textView, a następnie wywołałem metodę ogólnego przeznaczenia do modyfikowania tekstu, która owijała zmiany w textStorage wywołaniami shouldChangeTextInRange: i didChangeText:. Niektóre zmiany programowe dozwolone czyste undo, ale niektóre nie.

Druga (i może bardziej intuicyjna, ponieważ wprowadza zmiany, zanim tekst faktycznie pojawi się w textView), którą próbowałem, wykonywała manipulacje z wewnątrz metody delegate ' s shouldChangeTextInRange:, ponownie używając tej samej metody modyfikacji pamięci masowej ogólnego przeznaczenia, która owija zmiany w pamięci za pomocą wywołania shouldChangeTextInRange: i didChangeText:. Ponieważ te zmiany były uruchamiane pierwotnie z wewnątrz shouldChangeTextInRange:, ustawiłem flagę, która mówiła o wywołaniu wewnętrznym na shouldChangeTextInRange: być ignorowane, aby nie wchodzić w rekurencyjną czerń. Ponownie, niektóre zmiany programowe pozwalały na czyste undo, ale niektóre Nie (choć tym razem inne i na różne sposoby).

Z tym całym tłem, moje pytanie brzmi, czy ktoś może wskazać mi ogólną strategię programowego manipulowania przechowywaniem NSTextview, która utrzyma menedżera cofania w czystości i synchronizacji?

W której metodzie NSTextview Należy zwrócić uwagę na zmiany tekstu w textView (poprzez wpisywanie, wklejanie lub upuszczanie) i czy manipulacje do NSTextStorage? Czy jest to jedyny czysty sposób, aby to zrobić poprzez podklasowanie NSTextView lub NSTextStorage?

Author: Regexident, 2011-04-07

2 answers

Pierwotnie zamieściłem podobne pytanie dość niedawno (dzięki OP za zwrócenie stamtąd uwagi na to pytanie).

To pytanie nigdy nie było tak naprawdę odpowiedzi na moje zadowolenie, ale mam rozwiązanie mojego pierwotnego problemu, który, jak sądzę, odnosi się również do tego.

Moim rozwiązaniem nie jest użycie metod delegowania, ale raczej nadpisanie NSTextView. Wszystkie modyfikacje są wykonywane przez nadpisanie insertText: i replaceCharactersInRange:withString:

My insertText: override sprawdza tekst do wstawienia i decyduje, czy wstawić ten niezmodyfikowany, czy też dokonać innych zmian przed wstawieniem go. W każdym przypadku super ' s insertText: jest wywoływany do wykonania rzeczywistego wstawiania. Dodatkowo mój insertText: wykonuje własne grupowanie undo, w zasadzie przez wywołanie beginUndoGrouping: przed wstawieniem tekstu i endUndoGrouping: po. To brzmi zbyt prosto do pracy, ale wydaje się działać świetnie dla mnie. Rezultatem jest to, że na każdy wstawiony znak dostaje się jedną operację cofania (czyli ile działa" prawdziwych " edytorów tekstu-patrz TextMate, dla przykład). Dodatkowo powoduje to, że dodatkowe modyfikacje programowe są atomowe z operacją, która je uruchamia. Na przykład, jeśli użytkownik typuje {, a my insertText: programowo wstawia }, oba są zawarte w tej samej grupie Cofnij, więc jedno Cofnij odtwarza oba. Mój insertText: wygląda tak:

- (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 jest ivar używam do śledzenia, czy tekst jest wstawiany, czy nie. didHandleInsertOfString: i didHandleInsertOfString:withSelection: to funkcje, które w rzeczywistości wykonują insertText: wywołania w celu modyfikacji rzeczy. Oba są dość długie, ale pod koniec podam przykład.

Nadpisuję {[5] } ponieważ czasami używam tego wywołania do modyfikacji tekstu, a to omija cofanie. Możesz jednak podłączyć go z powrotem, aby cofnąć, wywołując shouldChangeTextInRange:replacementString:. Więc moje sterowanie to robi.

// 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: robi całą buncha, ale jej istotą jest to, że albo wstawia tekst (poprzez insertText: lub replaceCharactersInRange:withString:) i zwraca YES, jeśli wstawił, albo zwraca NO, jeśli nie wstawia. Wygląda coś takiego:

- (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;
}

Chciałbym zaznaczyć, że ten kod nie jest testowany w bitwie: nie używałem go w aplikacji wysyłkowej (jeszcze). Ale jest to przycięta wersja kodu, którego obecnie używam w projekcie, który zamierzam wysłać jeszcze w tym roku. Jak na razie wygląda na to, że działa dobrze.

Aby naprawdę zobaczyć, jak to działa, prawdopodobnie potrzebujesz przykładowego projektu, więc opublikowałem jeden na GitHubie.

 15
Author: zpasternack,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:08:41

Racja, nie jest to bynajmniej idealne rozwiązanie, ale jest to rozwiązanie w pewnym sensie.

Magazyn tekstowy aktualizuje menedżer cofania oparty na "grupach". Grupy te skupiają razem serię zmian (których nie pamiętam dokładnie z czubka głowy), ale pamiętam, że nowa jest tworzona po zmianie zaznaczenia.

Prowadzi to do możliwego rozwiązania, polegającego na szybkiej zmianie zaznaczenia na coś innego, a następnie przywróceniu go z powrotem. Nie jest idealnym rozwiązaniem, ale może być wystarczy, aby wymusić, aby magazyn tekstowy wypchnął nowy stan do menedżera cofania.

Przyjrzę się dokładniej i sprawdzę, czy nie mogę znaleźć/wyśledzić dokładnie, co się dzieje.

Edit: powinienem chyba wspomnieć, że minęło trochę czasu od kiedy używałem NSTextView i nie mam obecnie dostępu do Xcode na tym komputerze, aby sprawdzić, czy to działa nadal. Mam nadzieję, że tak będzie.

 0
Author: Tom Hancocks,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-05-25 09:16:28