17

我正在用 cocoa 编写一个特殊用途的文本编辑器,它可以执行自动文本替换、内联文本完成(ala Xcode)等操作。

我需要能够以编程方式操作NSTextView'sNSTextStorage以响应 1)用户输入,2)用户粘贴,3)用户删除文本。

我尝试了两种不同的通用方法,它们都导致NSTextView's 本机撤消管理器以不同的方式不同步。在每种情况下,我只使用NSTextView委托方法。我一直在努力避免子类化NSTextviewNSTextStorage(尽管如有必要我会子类化)。

我尝试的第一种方法是从textView delegate'textDidChange方法中进行操作。在该方法中,我分析了 中的更改,textView然后调用了一个通用方法来修改文本,该方法通过调用shouldChangeTextInRange:和调用将更改包装在 textStorage 中didChangeText:。一些编程更改允许完全撤消,但有些则不允许。

我尝试的第二种textView方法(也许更直观,因为它在文本实际出现在 s 中之前进行了更改)是从delegate'sshouldChangeTextInRange:方法中进行操作,再次使用相同的通用存储修改方法,该方法将存储中的更改包装为调用shouldChangeTextInRange:didChangeText:。由于这些更改最初是从内部触发的shouldChangeTextInRange:,因此我设置了一个标志,告诉内部调用shouldChangeTextInRange:被忽略,以免进入递归黑洞。同样,一些程序性更改允许完全撤消,但有些则不允许(尽管这次不同,并且方式不同)。

有了所有这些背景,我的问题是,有人可以指出一种以编程方式操作存储的通用策略,以NSTextview保持撤消管理器的清洁和同步?

我应该在哪个NSTextview委托方法中注意 textView 中的文本更改(通过键入、粘贴或删除)并对NSTextStorage? 或者是通过子类化NSTextViewor来做到这一点的唯一干净方法NSTextStorage

4

2 回答 2

17

我最近最初发布了一个类似的问题(感谢 OP 从那里指向这个问题)。

这个问题从来没有真正让我满意,但我确实有一个解决我原来的问题的方法,我相信这也适用于这个问题。

我的解决方案不是使用委托方法,而是覆盖NSTextView. 所有的修改都是通过覆盖insertText:replaceCharactersInRange:withString:

我的insertText:覆盖检查要插入的文本,并决定是插入未修改的文本,还是在插入之前进行其他更改。insertText:在任何情况下,都会调用super来进行实际插入。此外,我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 上发布了一个

于 2013-05-25T22:26:12.847 回答
0

是的,这绝不是一个完美的解决方案,但它是一种解决方案。

文本存储根据“组”更新撤消管理器。这些小组聚集在一起一系列编辑(我不太记得我的头顶),但是我确实记得在更改选择时会创建一个新的编辑。

这导致了一种可能的解决方案,即快速将选择更改为其他内容,然后将其恢复。这不是一个理想的解决方案,但它可能足以强制文本存储将新状态推送到撤消管理器。

我将进行更多的查看和调查,看看我是否无法找到/追踪到底发生了什么。

编辑:我可能应该提到,自从我使用 NSTextView 并且目前无法访问这台机器上的 Xcode 来验证它是否仍然有效以来,已经有一段时间了。希望它会。

于 2013-05-25T09:16:28.707 回答