2013-03-21 14 views
5

Sto creando una piccola applicazione server per OS X e sto utilizzando un NSTextView per registrare alcune informazioni sui client connessi.Scrolling NSTextView to bottom

Ogni volta che ho bisogno di registrare qualcosa che sto aggiungendo il nuovo messaggio al testo della NSTextView in questo modo:

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

Dopo questo vorrei il NSTextField (o forse dovrei dire il NSClipView che lo contiene) per scorrere verso il basso per mostrare l'ultima riga del suo testo (ovviamente dovrebbe scorrere solo se l'ultima riga non è ancora visibile, infatti se poi la nuova riga è la prima linea che registro è già presente sullo schermo quindi c'è non c'è bisogno di scorrere verso il basso).

Come posso farlo programmaticamente?

risposta

11

soluzione trovata:

- (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, ma non vedo la necessità di BOOL scroll "intelligente". In primo luogo, nell'espressione per lo scorrimento BOOL, l'operatore dovrebbe essere! = Invece di ==. Ha senso, e! = Funziona per me, ma == non funziona. In secondo luogo, se aggiungo una riga di testo, terminando con una nuova riga, a volte mostra la nuova riga e talvolta non lo fa. Non vedo perché vorremmo * non * voler "Scorrere fino alla fine del contenuto della textview". Questo è quello che vogliamo. In tutti i casi. Ho rimosso la linea if (scroll) e funziona perfettamente. Forse stiamo provando con i casi di bordo opposto :)) –

+1

Stai attento a usare "[self.textView scrollRangeToVisible: NSMakeRange (self.textView.string.length, 0)]". Questo potrebbe non scorrere effettivamente verso il basso (a seconda del layout del tuo NSTextView.) Se l'altezza di NSTextView non è equamente divisibile in base all'altezza delle righe di testo, è probabile che tagli parzialmente la riga inferiore del testo (in tal caso, lo scorrimento intelligente non funzionerà.) Migliore da usare (esempio Swift) : "self.textView.scrollToVisible (NSRect (x: 0, y: self.textView.frame.height-1, width: self.textView.frame.width, height: 1))". – pauln

+0

Inoltre, ho trovato utile questa lieve modifica al flag di scorrimento, che ti dà un po 'di flessibilità in modo da non dover essere esattamente in basso (esempio Swift): 'lascia scroll = abs (self.logTextView.visibleRect. maxY - self.logTextView.bounds.maxY) pauln

1

Sono stato scherzi con questo per un po ', perché non riuscivo a farlo funzionare in modo affidabile. Ho finalmente ottenuto il mio codice funzionante, quindi mi piacerebbe postarlo come risposta.

La mia soluzione consente di scorrere manualmente, mentre l'output viene aggiunto alla vista. Non appena si passa alla parte inferiore assoluta di NSTextView, lo scorrimento automatico riprende (se abilitato, ovvero).

Prima una categoria per #import questo solo quando necessario ...

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 

... crea un " OutputView ", che è una sottoclasse di 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 

... È possibile aggiungere un altro paio di metodi di convenienza per questa classe (ho spogliato in giù), in modo che è possibile emettere una stringa formattata.

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

A partire da OS 10.6 è semplice come nsTextView.scrollToEndOfDocument(self).

+0

Grazie! Puoi anche passare nil, invece di te stesso. –

+0

Sono d'accordo sul fatto che funzioni in pratica, ma non riesco a trovare alcun riferimento nella documentazione di Apple che mostri che questo metodo NSResponder è ufficialmente implementato (e supportato) da NSTextView. Potete fornire un link? – pauln

+0

Non posso credere di aver fatto casino per due giorni .... Grazie mille! – BadgerBadger

0

ho qualche metodo NSTextView e di input personalizzato su misura quindi la mia opzione era quella di utilizzare:

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