Cocoa:寻找编程操作NSTextView存储的一般策略,而不会弄乱undo

我在cocoa中编写了一个特殊用途的文本编辑器,它可以执行自动文本替换,内联文本完成(ala Xcode)等操作。 我需要能够以编程方式操作
NSTextView
NSTextStorage
以响应1)用户输入,2)用户粘贴,3)用户删除文本。 我尝试了两种不同的通用方法,它们都导致
NSTextView
的本机撤销管理器以不同的方式失去同步。在每种情况下,我只使用
NSTextView
委托方法。我一直试图避免子类化
NSTextview
NSTextStorage
(尽管我会在必要时进行子类化)。 我尝试的第一种方法是在
textView
delegate
textDidChange
方法中进行操作。从该方法中,我分析了ѭ6中已经改变的内容,然后调用了一个通用方法来修改文本,该文本包含了调用S10ѭ和
didChangeText:
的textStorage中的更改。一些程序化更改允许清除撤消,但有些没有。 第二个(也许更直观,因为它在文本实际出现在
textView
之前进行了更改)我试过的方法是在
delegate
shouldChangeTextInRange:
方法中进行操作,再次使用相同的通用存储修改方法,包装存储中的更改打电话给
shouldChangeTextInRange:
didChangeText:
。由于这些变化最初是在
shouldChangeTextInRange:
内触发的,所以我设置了一个标志,告诉内部调用
shouldChangeTextInRange:
被忽略,以免输入递归黑洞。同样,一些程序化更改允许清除撤消,但有些没有(虽然这次不同,并以不同的方式)。 有了这样的背景,我的问题是,是否有人能指出我以编程方式操纵
NSTextview
的存储的一般策略,这将使撤消管理器保持清洁和同步? 其中
NSTextview
委托方法我应该注意textView中的文本更改(通过键入,粘贴或删除)并对
NSTextStorage
进行操作?或者是通过继承
NSTextView
NSTextStorage
来做到这一点的唯一干净方法?     
已邀请:
我最近最近发布了一个类似的问题(感谢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:
做了整个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上发布了一个。     
对,这绝不是一个完美的解决方案,但它是一种解决方案。 文本存储基于“组”更新撤消管理器。这些组聚集在一起进行了一系列编辑(我不太记得我的头脑),但我确实记得在选择被更改时会创建一个新编辑。 这导致可能的解决方案,快速将选择更改为其他内容,然后将其还原。不是一个理想的解决方案,但它可能足以强制文本存储将新状态推送到撤消管理器。 我将进行更多的研究和调查,看看我是否无法找到/追踪到底发生了什么。 编辑:我可能会提到它已经有一段时间了,因为我已经使用了NSTextView,并且目前无法在此计算机上访问Xcode以验证其是否仍可正常工作。希望它会。     

要回复问题请先登录注册