How to make NSTextView balance delimiters with a double-click?

≯℡__Kan透↙ 提交于 2020-01-17 12:38:51

问题


It's common to have a text editor for code or other structured content that balances delimiters of some sort; when you double click on a { it selects to the matching }, or similarly for ( ) pairs, [ ] pairs, etc. How can I implement this behavior in NSTextView in Cocoa/Obj-C?

(I will be posting an answer momentarily, since I found nothing on SO about this and spent today implementing a solution. Better answers are welcome.)

ADDENDUM:

This is not the same as this question, which is about NSTextField and is primarily concerned with NSTextField and field editor issues. If that question is solved by substituting a custom NSTextView subclass into the field editor, then that custom subclass could use the solution given here, of course; but there might be many other ways to solve the problem for NSTextField, and substituting a custom NSTextView subclass into the field editor is not obviously the right solution to that problem, and in any case a programmer concerned with delimiter balancing in NSTextView (which is presumably the more common problem) could care less about all of those NSTextField and field editor issues. So that is a different question – although I will add a link from that question to this one, as one possible direction it could go.

This is also not the same as this question, which is really about changing the definition of a "word" in NSTextView when a double-click occurs. As per Apple's documentation, these are different problems with different solutions; for delimiter-balancing (this question) Apple specifically recommends the use of NSTextView's selectionRangeForProposedRange:granularity: method, whereas for changing the definition of a word (that question) Apple specifically states that the selectionRangeForProposedRange:granularity: method should not be used.


回答1:


In their Cocoa Text Architecture Guide (https://developer.apple.com/library/prerelease/mac/documentation/TextFonts/Conceptual/CocoaTextArchitecture/TextEditing/TextEditing.html), Apple suggests subclassing NSTextView and overriding selectionRangeForProposedRange:granularity: to achieve this sort of thing; they even say "For example, in a code editor you can provide a delegate that extends a double click on a brace or parenthesis character to its matching delimiter." However, it is not immediately clear how to achieve this, since you want the delimiter match to happen only at after a simple double-click on a delimiter, not after a double-click-drag or even a double-click-hold-release.

The best solution I could come up with involves overriding mouseDown: as well, and doing a little bookkeeping about the state of affairs. Maybe there is a simpler way. I've left out the core part of the code where the delimiter match actually gets calculated; that will depend on what delimiters you're matching, what syntactical complexities (strings, comments) might exist, and so forth. In my code I actually call a tokenizer to get a token stream, and I use that to find the matching delimiter. YMMV. So, here's what I've got:

In your NSTextView subclass interface (or class extension, better yet):

// these are used in selectionRangeForProposedRange:granularity:
// to balance delimiters properly
BOOL inEligibleDoubleClick;
NSTimeInterval doubleDownTime;

In your NSTextView subclass implementation:

- (void)mouseDown:(NSEvent *)theEvent
{
    // Start out willing to work with a double-click for delimiter-balancing;
    // see selectionRangeForProposedRange:proposedCharRange granularity: below
    inEligibleDoubleClick = YES;

    [super mouseDown:theEvent];
}

- (NSRange)selectionRangeForProposedRange:(NSRange)proposedCharRange
    granularity:(NSSelectionGranularity)granularity
{
    if ((granularity == NSSelectByWord) && inEligibleDoubleClick)
    {
        // The proposed range has to be zero-length to qualify
        if (proposedCharRange.length == 0)
        {
            NSEvent *event = [NSApp currentEvent];
            NSEventType eventType = [event type];
            NSTimeInterval eventTime = [event timestamp];

            if (eventType == NSLeftMouseDown)
            {
                // This is the mouseDown of the double-click; we do not want
                // to modify the selection here, just log the time
                doubleDownTime = eventTime;
            }
            else if (eventType == NSLeftMouseUp)
            {
                // After the double-click interval since the second mouseDown,
                // the mouseUp is no longer eligible
                if (eventTime - doubleDownTime <= [NSEvent doubleClickInterval])
                {
                    NSString *scriptString = [[self textStorage] string];

                    ...insert delimiter-finding code here...
                    ...return the matched range, or NSBeep()...
                }
                else
                {
                    inEligibleDoubleClick = false;
                }
            }
            else
            {
                inEligibleDoubleClick = false;
            }
        }
        else
        {
            inEligibleDoubleClick = false;
        }
    }

    return [super selectionRangeForProposedRange:proposedCharRange
        granularity:granularity];
}

It's a little fragile, because it relies on NSTextView's tracking working in a particular way and calling out to selectionRangeForProposedRange:granularity: in a particular way, but the assumptions are not large; I imagine it's pretty robust.



来源:https://stackoverflow.com/questions/32673991/how-to-make-nstextview-balance-delimiters-with-a-double-click

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!