可可:寻找一种用于NSTextView存储的程序化操作的一般策略,而不会弄乱撤消 [英] Cocoa: looking for a general strategy for programmatic manipulation of NSTextView storage without messing up undo

查看:103
本文介绍了可可:寻找一种用于NSTextView存储的程序化操作的一般策略,而不会弄乱撤消的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在用可可编写一种专用的文本编辑器,它可以执行自动文本替换,内联文本完成(ala Xcode )等操作.

I am writing a special-purpose text editor in cocoa that does things like automatic text substitution, inline text completions (ala Xcode), etc.

我需要能够以编程方式操作NSTextViewNSTextStorage,以响应1)用户键入,2)用户粘贴,3)用户删除文本.

I need to be able to programmatically manipulate the NSTextView’s NSTextStorage in response to 1) user typing, 2) user pasting, 3) user dropping text.

我尝试了两种不同的通用方法,它们都导致NSTextView的本机撤消管理器以不同的方式不同步.在每种情况下,我仅使用NSTextView委托方法.我一直在尝试避免对NSTextviewNSTextStorage进行子类化(尽管如有必要,我将对其进行子类化).

I have tried two different general approaches and both of them have caused the NSTextView’s native undo manager to get out of sync in different ways. In each case, I am only using NSTextView delegate methods. I have been trying to avoid subclassing NSTextview or NSTextStorage (though I will subclass if necessary).

我尝试的第一种方法是在textView delegatetextDidChange方法中进行操作.从该方法中,我分析了textView中的更改,然后称为用于修改文本的通用方法,该方法将对shouldChangeTextInRange:didChangeText:的调用包装在textStorage中.某些程序性更改允许清除撤消操作,但某些不允许.

The first approach I tried was doing the manipulations from within the textView delegate’s textDidChange method. From within that method, I analyzed what had been changed in the textView and then called a general purpose method for modifying text that wrapped the changes in the textStorage with calls to shouldChangeTextInRange: and didChangeText:. Some of the programmatic changes allowed clean undo’s but some did not.

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

The second (and maybe more intuitive because it makes changes before the text actually appears in the textView) approach I tried was doing the manipulations from within the delegate’s shouldChangeTextInRange: method, again using the same general purpose storage modification method that wraps changes in the storage with a call to shouldChangeTextInRange: and didChangeText:. Since these changes were being triggered originally from within shouldChangeTextInRange:, I set a flag that told the inner call to shouldChangeTextInRange: to be ignored so as not to enter recursive blackholeness. Again, Some of the programmatic changes allowed clean undo’s but some did not (though different ones this time, and in different ways).

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

With all that background, my question is, can someone point me to a general strategy for programmatically manipulating the storage of an NSTextview that will keep the undo manager clean and in sync?

在哪个NSTextview委托方法中,我应该注意textView中的文本更改(通过键入,粘贴或放置),并对NSTextStorage进行操作吗?还是通过子类化NSTextViewNSTextStorage做到这一点的唯一干净方法?

In which NSTextview delegate method should I pay attention to the text changes in the textView (via typing, pasting, or dropping) and do the manipulations to the NSTextStorage? Or is the only clean way to do this by subclassing either NSTextView or NSTextStorage?

推荐答案

我最初发布了

I originally posted a similar question fairly recently (thanks to OP for pointing from there back to this question).

这个问题从未真正令我满意,但我确实可以解决我原来的问题,我认为这也适用于此.

That question was never really answered to my satisfaction, but I do have a solution to my original problem which I believe also applies to this.

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

My solution is not use to the delegate methods, but rather to override NSTextView. All of the modifications are done by overriding insertText: and replaceCharactersInRange:withString:

我的insertText:替代检查要插入的文本,并决定是插入未修改的文本,还是在插入之前进行其他更改.无论如何,将调用super的insertText:进行实际插入.此外,我的insertText:本身就是撤消分组,基本上是通过在插入文本之前调用beginUndoGrouping:以及在插入文本之后调用endUndoGrouping:来完成的.这听起来太简单了,无法工作,但对我来说似乎很有用.结果是,每个插入的字符得到一个撤消操作(这是多少个实际"文本编辑器起作用-例如,参见TextMate).另外,这使附加的程序修改与触发它们的操作原子化.例如,如果用户键入{,并且我的insertText:以编程方式插入},则两者都包含在同一撤消分组中,因此一个撤消会撤消这两个撤消.我的insertText:看起来像这样:

My insertText: override inspects the text to be inserted, and decides whether to insert that unmodified, or do other changes before inserting it. In any case super's insertText: is called to do the actual insertion. Additionally, my insertText: does it's own undo grouping, basically by calling beginUndoGrouping: before inserting text, and endUndoGrouping: after. This sounds way too simple to work, but it appears to work great for me. The result is that you get one undo operation per character inserted (which is how many "real" text editors work - see TextMate, for example). Additionally, this makes the additional programmatic modifications atomic with the operation that triggers them. For example, if the user types {, and my insertText: programmatically inserts }, both are included in the same undo grouping, so one undo undoes both. My insertText: looks like this:

- (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:调用来修改内容的函数.它们都很长,但最后我将举例说明.

insertingText is an ivar I'm using to keep track of whether text is being inserted or not. didHandleInsertOfString: and didHandleInsertOfString:withSelection: are the functions that actually end up doing the insertText: calls to modify stuff. They're both pretty long, but I'll include an example at the end.

我只覆盖replaceCharactersInRange:withString:,因为有时我使用该调用来修改文本,并且绕过了撤消.但是,您可以通过调用shouldChangeTextInRange:replacementString:将其挂接起来以撤消.因此,我的替代做到了.

I'm only overriding replaceCharactersInRange:withString: because I sometimes use that call to do modification of text, and it bypasses undo. However, you can hook it back up to undo by calling shouldChangeTextInRange:replacementString:. So my override does that.

// 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:会执行很多Buna的工作,但要点是它要么插入文本(通过insertText:replaceCharactersInRange:withString:),如果插入了任何内容,则返回YES,否则,则返回NO.插入.看起来像这样:

didHandleInsertOfString: does a whole buncha stuff, but the gist of it is that it either inserts text (via insertText: or replaceCharactersInRange:withString:), and returns YES if it did any insertion, or returns NO if it does no insertion. It looks something like this:

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

我要指出的是,此代码尚未经过实战测试:我尚未在运输应用程序中使用过它(尚未).但这是我打算在今年晚些时候发布的项目中使用的精简版代码.到目前为止,它似乎运行良好.

I would point out that this code isn't battle-tested: I've not used it in a shipping app (yet). But it is a trimmed down version of code I'm currently using in a project I intend to ship later this year. So far it appears to work well.

为了真正了解它是如何工作的,您可能需要一个示例项目,所以我已经在github上发布了一个.

In order to really see how this works you probably want an example project, so I've posted one on github.

这篇关于可可:寻找一种用于NSTextView存储的程序化操作的一般策略,而不会弄乱撤消的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆