Scrolling NSTextView to bottom

I am creating a small server application for OS X, and I am using NSTextView to log information about connected clients.

Whenever I need to write something, I add a new message to the NSTextView text as follows:

- (void)logMessage:(NSString *)message
{
    if (message) {
        self.textView.string = [self.textView.string stringByAppendingFormat:@"%@\n",message];
    }
}

After that, I would like the NSTextField (or maybe I should say that the NSClipView that contains it) scrolls down to display the last line of its text (obviously, it should only scroll if the last line is still not visible, fact, if then the new line is the first line that I register, it is already on the screen, so there is no need to scroll down).

How can I do this programmatically?

+10
source share
5

:

- (void)logMessage:(NSString *)message
{
    if (message) {
        [self appendMessage:message];
    }
}

- (void)appendMessage:(NSString *)message
{
    NSString *messageWithNewLine = [message stringByAppendingString:@"\n"];

    // Smart Scrolling
    BOOL scroll = (NSMaxY(self.textView.visibleRect) == NSMaxY(self.textView.bounds));

    // Append string to textview
    [self.textView.textStorage appendAttributedString:[[NSAttributedString alloc]initWithString:messageWithNewLine]];

    if (scroll) // Scroll to end of the textview contents
        [self.textView scrollRangeToVisible: NSMakeRange(self.textView.string.length, 0)];
}
+15

OS 10.6, , nsTextView.scrollToEndOfDocument(self).

+9

4 + 5

let smartScroll = self.textView.visibleRect.maxY == self.textView.bounds.maxY

self.textView.textStorage?.append("new text")

if smartScroll{
    self.textView.scrollToEndOfDocument(self)
}

+3

, . , , , .

, . NSTextView, ( , ).

#import , ...

FSScrollToBottomExtensions.h:

@interface NSView (FSScrollToBottomExtensions)
- (float)distanceToBottom;
- (BOOL)isAtBottom;
- (void)scrollToBottom;
@end

FSScrollToBottomExtensions.m:

@implementation NSView (FSScrollToBottomExtensions)
- (float)distanceToBottom
{
    NSRect  visRect;
    NSRect  boundsRect;

    visRect = [self visibleRect];
    boundsRect = [self bounds];
    return(NSMaxY(visRect) - NSMaxY(boundsRect));
}

// Apple suggestion did not work for me.
- (BOOL)isAtBottom
{
    return([self distanceToBottom] == 0.0);
}

// The scrollToBottom method provided by Apple seems unreliable, so I wrote this one
- (void)scrollToBottom
{
    NSPoint     pt;
    id          scrollView;
    id          clipView;

    pt.x = 0;
    pt.y = 100000000000.0;

    scrollView = [self enclosingScrollView];
    clipView = [scrollView contentView];

    pt = [clipView constrainScrollPoint:pt];
    [clipView scrollToPoint:pt];
    [scrollView reflectScrolledClipView:clipView];
}
@end

... "OutputView", NSTextView:

FSOutputView.h:

@interface FSOutputView : NSTextView
{
    BOOL                scrollToBottomPending;
}

FSOutputView.m:

@implementation FSOutputView

- (id)setup
{
    ...
    return(self);
}

- (id)initWithCoder:(NSCoder *)aCoder
{
    return([[super initWithCoder:aCoder] setup]);
}

- (id)initWithFrame:(NSRect)aFrame textContainer:(NSTextContainer *)aTextContainer
{
    return([[super initWithFrame:aFrame textContainer:aTextContainer] setup]);
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [super dealloc];
}

- (void)awakeFromNib
{
    NSNotificationCenter    *notificationCenter;
    NSView                  *view;

    // viewBoundsDidChange catches scrolling that happens when the caret
    // moves, and scrolling caused by pressing the scrollbar arrows.
    view = [self superview];
    [notificationCenter addObserver:self
    selector:@selector(viewBoundsDidChangeNotification:)
        name:NSViewBoundsDidChangeNotification object:view];
    [view setPostsBoundsChangedNotifications:YES];

    // viewFrameDidChange catches scrolling that happens because text
    // is inserted or deleted.
    // it also catches situations, where window resizing causes changes.
    [notificationCenter addObserver:self
        selector:@selector(viewFrameDidChangeNotification:)
        name:NSViewFrameDidChangeNotification object:self];
    [self setPostsFrameChangedNotifications:YES];

}

- (void)handleScrollToBottom
{
    if(scrollToBottomPending)
    {
        scrollToBottomPending = NO;
        [self scrollToBottom];
    }
}

- (void)viewBoundsDidChangeNotification:(NSNotification *)aNotification
{
    [self handleScrollToBottom];
}

- (void)viewFrameDidChangeNotification:(NSNotification *)aNotification
{
    [self handleScrollToBottom];
}

- (void)outputAttributedString:(NSAttributedString *)aAttributedString
    flags:(int)aFlags
{
    NSRange                     range;
    BOOL                        wasAtBottom;

    if(aAttributedString)
    {
        wasAtBottom = [self isAtBottom];

        range = [self selectedRange];
        if(aFlags & FSAppendString)
        {
            range = NSMakeRange([[self textStorage] length], 0);
        }
        if([self shouldChangeTextInRange:range
            replacementString:[aAttributedString string]])
        {
            [[self textStorage] beginEditing];
            [[self textStorage] replaceCharactersInRange:range
                withAttributedString:aAttributedString];
            [[self textStorage] endEditing];
        }

        range.location += [aAttributedString length];
        range.length = 0;
        if(!(aFlags & FSAppendString))
        {
            [self setSelectedRange:range];
        }

        if(wasAtBottom || (aFlags & FSForceScroll))
        {
            scrollToBottomPending = YES;
        }
    }
}
@end

... You can add a few more convenience methods for this class (I turned it off) so that you can output a formatted string.

- (void)outputString:(NSString *)aFormatString arguments:(va_list)aArguments attributeKey:(NSString *)aKey flags:(int)aFlags
{
    NSMutableAttributedString   *str;

    str = [... generate attributed string from parameters ...];
    [self outputAttributedString:str flags:aFlags];
}

- (void)outputLineWithFormat:(NSString *)aFormatString, ...
{
    va_list         args;
    va_start(args, aFormatString);
    [self outputString:aFormatString arguments:args attributeKey:NULL flags:FSAddNewLine];
    va_end(args);
}
+2
source

I have a custom NSTextView method and user input, so my option was to use:

self.scrollView.contentView.scroll(NSPoint(x: 1, y: self.textView.frame.size.height))
0
source

All Articles