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);
}
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 :)) –
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
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