2013-03-21 16 views
5

Tworzę małą aplikację serwerową dla OS X i używam NSTextView do rejestrowania informacji o połączonych klientach.Przewijanie NSTextView do dołu

Kiedy muszę coś mi dołączenie nowej wiadomości do tekstu NSTextView tędy log:

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

Po tym chciałbym się NSTextField (a może powinienem powiedzieć, że NSClipView zawiera go), aby przewinąć w dół, aby pokazać ostatni wiersz jego tekstu (oczywiście powinien on przewijać tylko wtedy, gdy ostatnia linia nie jest jeszcze widoczna, w rzeczywistości, jeśli wtedy nowa linia jest pierwszą linią, to loguję, że jest już na ekranie, więc jest nie trzeba przewijać w dół).

Jak mogę to zrobić programowo?

Odpowiedz

11

Znaleziono rozwiązanie:

- (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)]; 
} 
+0

OK, ale nie widzę potrzebę „inteligentne” BOOL zwoju. Po pierwsze, w wyrażeniu do przewijania BOOL, operator powinien być! = Zamiast ==. Ma sens, a! = Działa dla mnie, ale == nie działa. Po drugie, jeśli dodaję jeden wiersz tekstu, kończąc na znaku nowej linii, to czasami pokazuje on znak nowej linii, a czasem nie. Nie rozumiem, dlaczego kiedykolwiek * nie * chcemy "Przewiń do końca zawartości w tekście". Tego właśnie chcemy. We wszystkich przypadkach. Usunąłem linię if (scroll) i działa dobrze. Być może testujemy z przypadkami przeciwległymi :)) –

+1

Uważaj przy użyciu "[self.textView scrollRangeToVisible: NSMakeRange (self.textView.string.length, 0)]". To nie może faktycznie przewinąć do dołu (w zależności od układu twojego NSTextView.Jeśli wysokość NSTextView nie jest równomiernie podzielona przez wysokość linii tekstu, prawdopodobnie częściowo odetnie dolną linię tekstu (w takim przypadku inteligentne przewijanie nie zadziała.) Lepiej używać (przykład Swift) : "self.textView.scrollToVisible (NSRect (x: 0, y: self.textView.frame.height-1, width: self.textView.frame.width, height: 1))". – pauln

+0

Zauważyłem również, że ta drobna modyfikacja flagi przewijania jest pomocna, co daje trochę luzu, więc nie musisz być dokładnie na dole (przykład Swift): 'let scroll = abs (self.logTextView.visibleRect. maxY - self.logTextView.bounds.maxY) pauln

1

Byłem brudząc przy tym na chwilę, bo nie mogłem zmusić go do pracy w wiarygodny sposób. W końcu udało mi się uruchomić mój kod, więc chciałbym opublikować go jako odpowiedź.

Moje rozwiązanie umożliwia przewijanie ręczne, podczas gdy dane wyjściowe są dodawane do widoku. Jak tylko przewiniesz do absolutnego dołu NSTextView, automatyczne przewijanie zostanie wznowione (jeśli jest włączone).

Pierwsza kategoria do #import to tylko w razie potrzeby ...

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's 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 

... utwórz własne " OutputView ", który jest podklasą 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 

... Można dodać kilka więcej metod wygody do tej klasy (mam pozbawiono go w dół), tak, że można wyprowadzać sformatowany ciąg.

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

Jak OS 10.6 jest to tak proste jak nsTextView.scrollToEndOfDocument(self).

+0

Dzięki! Możesz także przekazać zero, zamiast siebie. –

+0

Zgadzam się, że to działa w praktyce, ale nie mogę znaleźć żadnego odniesienia do tego w dokumentacji Apple'a pokazującej, że ta metoda NSResponder jest oficjalnie zaimplementowana (i obsługiwana) przez NSTextView. Czy możesz podać link? – pauln

+0

Nie mogę uwierzyć, że mam z tym problemy przez dwa dni ... Dziękuję bardzo! – BadgerBadger

0

Mam dostosowanego NSTextView i niestandardowe metody wejściowej więc moja opcja była w użyciu:

self.scrollView.contentView.scroll(NSPoint(x: 1, y: self.textView.frame.size.height)) 
Powiązane problemy