NSTextFieldCell's cellSizeForBounds: doesn't match wrapping behavior? - cocoa

It seems to be commonly accepted that cellSizeForBounds: allows one to calculate a text field's "natural" size. However, for NSTextField, I've found that it doesn't quite match:
#interface MyTextField : NSTextField #end
#implementation MyTextField
- (void)textDidChange:(NSNotification *)notification {
[super textDidChange:notification];
[self validateEditing]; // Forces updating from the field editor
NSSize cellSize = [self.cell cellSizeForBounds:
NSMakeRect(0, 0, self.bounds.size.width, CGFLOAT_MAX)];
NSRect frame = self.frame;
CGFloat heightDelta = cellSize.height - frame.size.height;
frame.size.height += heightDelta;
if (!self.superview.flipped) { frame.origin.y -= heightDelta; }
self.frame = frame;
}
#end
(Note that I'm not using Auto Layout, but the principle is the same. This problem doesn't happen with every string, but it is pretty easy to reproduce.)
I suspect this is because of the text field's border, which adds an extra offset. Is there any way to automatically compute the relationship between cellSizeForBounds: and the NSTextField's frame? How else might I solve this issue?

There is a difference between the cell size and the frame size. The best way to determine it is to ask a text field to size itself to its content using -sizeToFit and then compare its cell size to its frame size. You may want to do this with a secondary, off-screen text field. Be sure to configure all of its parameters identically to the text field you're intending to resize. Also, the cell size will be an "exact" fit, meaning it will potentially have fractional width or height. The frame size resulting from -sizeToFit will be integral. So, you should apply ceil() to the cell size components before comparing to the frame size to compute the border size.
In theory, you only have to do this once for a given text field configuration / style.

Related

How to do batch display using NSTextView

I'd like to be able to show a view that resembles something like a console log, with multiple lines of text that are scrollable and selectable.
The fundamental procedure I have in mind is maintaining an array of strings (call it lines) and appending these to the textStorage of the NSTextView using a new line character as delimiter.
However there are a few factors to consider, such as:
Updating the textStorage on scroll so that it appears seamless to the user
Updating the textStorage on resizing the view height
Maintaining scroll position after the textStorage gets updated
Handling an out of memory possibility
Can someone please provide some guidance or a sample to get me started?
Add a string from your array to the NSTextStorage and animate the NSClipView bounds origin.
- (void)appendText:(NSString*)string {
// Add a newline, if you need to
string = [NSString stringWithFormat:#"%#\n", string];
// Find range
[self.textView.textStorage replaceCharactersInRange:NSMakeRange(self.textView.textStorage.string.length, 0) withString:string];
// Get clip view
NSClipView *clipView = self.textView.enclosingScrollView.contentView;
// Calculate the y position by subtracting
// clip view height from total document height
CGFloat scrollTo = self.textView.frame.size.height - clipView.frame.size.height;
// Animate bounds
[[clipView animator] setBoundsOrigin:NSMakePoint(0, scrollTo)];
}
If you have elasticity set in your NSTextView you need to monitor for its frame changes to get exact results. Add frameDidChange listener to your text view and animate in the handler:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Text view setup
[_textView setPostsFrameChangedNotifications:YES];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(scrollToBottom) name:NSViewFrameDidChangeNotification object:_textView];
}
- (void)appendText:(NSString*)string {
// Add a newline, if you need to
string = [NSString stringWithFormat:#"%#\n", string];
// Find range
[self.textView.textStorage replaceCharactersInRange:NSMakeRange(self.textView.textStorage.string.length, 0) withString:string];
}
- (void)scrollToBottom {
// Get the clip view and calculate y position
NSClipView *clipView = self.textView.enclosingScrollView.contentView;
// Y position for bottom is document's height - viewport height
CGFloat scrollTo = self.textView.frame.size.height - clipView.frame.size.height;
[[clipView animator] setBoundsOrigin:NSMakePoint(0, scrollTo)];
}
In real-life application you would probably need to set some sort of threshold to see if the user has scrolled away from the end more than the height of a line.

CATextLayer gets rasterized too early and it is blurred

I have some troubles with CATextLayer, that could be due to me, but I didn't find any help on this topic. I am on OS X (on iOS it should be the same).
I create a CATextLayer layers with scale factor > 1 and what I get is a blurred text. The layer is rasterized before applying the scale, I think. Is this the expected behavior? I hope it is not, because it just makes no sense... A CAShapeLayer is rasterized after that its transformation matrix is applied, why the CATextLayer should be different?
In case I am doing something wrong... what is it??
CATextLayer *layer = [CATextLayer layer];
layer.string = #"I like what I am doing";
layer.font = (__bridge CFTypeRef)[NSFont systemFontOfSize:24];
layer.fontSize = 24;
layer.anchorPoint = CGPointZero;
layer.frame = CGRectMake(0, 0, 400, 100);
layer.foregroundColor = [NSColor blackColor].CGColor;
layer.transform = CATransform3DMakeScale(2., 2., 1.);
layer.shouldRasterize = NO;
[self.layer addSublayer:layer];
The solution I use at the moment is to set the contentsScale property of the layer to the scale factor. The problem is that this solution doesn't scale: if the scale factor of any of the parent layers changes, then contentsScale should be updated too. I should write code to traverse the layers tree to update the contentsScale properties of all CATextLayers... not exactly what I would like to do.
Another solution, that is not really a solution, is to convert the text to a shape and use a CAShapeLayer. But then I don't see the point of having CATextLayers.
A custom subclass of CALayer could help in solving this problem?
EDIT: Even CAGradientLayer is able to render its contents, like CAShapeLayer, after that its transformation matrix is applied. Can someone explain how it is possible?
EDIT 2: My guess is that paths and gradients are rendered as OpenGL display lists, so they are rasterized at the actual size on the screen by OpenGL itself. Texts are rasterized by Core Animation, so they are bitmaps for OpenGL.
I think that I will go with the contentsScale solution for the moment. Maybe, in the future, I will convert texts to shapes. In order to get best results with little work, this is the code I use now:
[CATransaction setDisableActions:YES];
CGFloat contentsScale = ceilf(scaleOfParentLayer);
// _scalableTextLayer is a CATextLayer
_scalableTextLayer.contentsScale = contentsScale;
[_scalableTextLayer displayIfNeeded];
[CATransaction setDisableActions:NO];
After trying all the approaches, the solution I am using now is a custom subclass of CALayer. I don't use CATextLayer at all.
I override the contentsScale property with this custom setter method:
- (void)setContentsScale:(CGFloat)cs
{
CGFloat scale = MAX(ceilf(cs), 1.); // never less than 1, always integer
if (scale != self.contentsScale) {
[super setContentsScale:scale];
[self setNeedsDisplay];
}
}
The value of the property is always rounded to the upper integer value. When the rounded value changes, then the layer must be redrawn.
The display method of my CALayer subclass creates a bitmap image of the size of the text multiplied by the contentsScale factor and by the screen scale factor.
- (void)display
{
CGFloat scale = self.contentsScale * [MyUtils screenScale];
CGFloat width = self.bounds.size.width * scale;
CGFloat height = self.bounds.size.height * scale;
CGContextRef bitmapContext = [MyUtils createBitmapContextWithSize:CGSizeMake(width, height)];
CGContextScaleCTM(bitmapContext, scale, scale);
CGContextSetShouldSmoothFonts(bitmapContext, 0);
CTLineRef line = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)(_text));
CGContextSetTextPosition(bitmapContext, 0., self.bounds.size.height-_ascender);
CTLineDraw(line, bitmapContext);
CFRelease(line);
CGImageRef image = CGBitmapContextCreateImage(bitmapContext);
self.contents = (__bridge id)(image);
CGImageRelease(image);
CGContextRelease(bitmapContext);
}
When I change the scale factor of the root layer of my hierarchy, I loop on all text layers and set the contentsScale property to the same factor. The display method is called only if the rounded value of the scale factor changes (i.e. if the previous value was 1.6 and now I set 1.7, nothing happens. But if the new value is 2.1, then the layer is redisplayed).
The cost in terms of speed of the redraw is little. My test is to change continuously the scale factor of a hierarchy of 40 text layers on an 3rd gen. iPad. It works like butter.
CATextLayer is different because the underlying CoreText renders the glyphs with the specified font size (educated guess based on experiments).
You could add an action to the parent layer so as soon as it's scale changes, it changes the font size of the text layer.
Blurriness could also come from misaligned pixels. That can happen if you put the text layer to non integral position or any transformation in the superlayer hierarchy.
Alternatively you could subclass CALayer and then draw the text using Cocoa in drawInContext:
see example here:
http://lists.apple.com/archives/Cocoa-dev/2009/Jan/msg02300.html
http://people.omnigroup.com/bungi/TextDrawing-20090129.zip
If you want to have the exact behaviour of a CAShapeLayer then you will need to convert your string into a bezier path and have CAShapeLayer render it. It's a bit of work but then you will have the exact behaviour you are looking for. An alternate approach, is to scale the fontSize instead. This yields crisp text every time but it might not fit to you exact situation.
To draw text as CAShapeLayer have a look at Apple Sample Code "CoreAnimationText":
http://developer.apple.com/library/mac/#samplecode/CoreAnimationText/Listings/Readme_txt.html

How to auto-resize NSWindow based on auto layout contentView's minimum size

At the moment the behaviour of my app allows me to called this extra function I wrote that hacks resizes the window to fittingSize.
- (void)fitToMinimumSize
{
NSRect frame = [self frame];
frame.size = [[self contentView] fittingSize];
int originalHeight = [self frame].size.height;
int diff = originalHeight - frame.size.height;
frame.origin.y += diff;
[self setFrame:frame display:YES];
}
But can I automate this behaviour through some built in auto layout code so that the window is always the size of the minimum of it's contentView instead of this almighty hack?
Edit: I've found out I can check [[self contentView] fittingSize], but how can I observe this incase it changes, or should I be triggering it myself?
Normally, everything should already be 'automatic' if your layout is fully and unambiguously determined by NSConstraint's set on your window's content view and its children.
In your case I'd try installing a height and a width constraints on the content view with sufficiently small dimensions (maybe even zero), but with priority lower than the 'content compression resistance priority' set on your subviews. To have everything working properly, you may have to play with constraint priorities and/or install additional constraints on the content view's children.
This should work without any other code, as long as your subviews are constrained to resist collapsing to zero. I'd also think that in this case the window should not be user-resizeable.

How to let NSTextField grow with the text in auto layout?

Auto layout in Lion should make it fairly simple to let a text field (and hence a label) grow with text it holds.
The text field is set to wrap in Interface Builder.
What is a simple and reliable way to do this?
The method intrinsicContentSize in NSView returns what the view itself thinks of as its intrinsic content size.
NSTextField calculates this without considering the wraps property of its cell, so it will report the dimensions of the text if laid out in on a single line.
Hence, a custom subclass of NSTextField can override this method to return a better value, such as the one provided by the cell's cellSizeForBounds: method:
-(NSSize)intrinsicContentSize
{
if ( ![self.cell wraps] ) {
return [super intrinsicContentSize];
}
NSRect frame = [self frame];
CGFloat width = frame.size.width;
// Make the frame very high, while keeping the width
frame.size.height = CGFLOAT_MAX;
// Calculate new height within the frame
// with practically infinite height.
CGFloat height = [self.cell cellSizeForBounds: frame].height;
return NSMakeSize(width, height);
}
// you need to invalidate the layout on text change, else it wouldn't grow by changing the text
- (void)textDidChange:(NSNotification *)notification
{
[super textDidChange:notification];
[self invalidateIntrinsicContentSize];
}
Swift 4
Editable Autosizing NSTextField
Based on Peter Lapisu's Objective-C post
Subclass NSTextField, add the code below.
override var intrinsicContentSize: NSSize {
// Guard the cell exists and wraps
guard let cell = self.cell, cell.wraps else {return super.intrinsicContentSize}
// Use intrinsic width to jive with autolayout
let width = super.intrinsicContentSize.width
// Set the frame height to a reasonable number
self.frame.size.height = 750.0
// Calcuate height
let height = cell.cellSize(forBounds: self.frame).height
return NSMakeSize(width, height);
}
override func textDidChange(_ notification: Notification) {
super.textDidChange(notification)
super.invalidateIntrinsicContentSize()
}
Setting self.frame.size.height to 'a reasonable number' avoids some bugs when using FLT_MAX, CGFloat.greatestFiniteMagnitude or large numbers. The bugs occur during operation when the user select highlights the text in the field, they can drag scroll up and down off into infinity. Additionally when the user enters text the NSTextField is blanked out until the user ends editing. Finally if the user has selected the NSTextField and then attempts to resize the window, if the value of self.frame.size.height is too large the window will hang.
The accepted answer is based on manipulating intrinsicContentSize but that may not be necessary in all cases. Autolayout will grow and shrink the height of the text field if (a) you give the text field a preferredMaxLayoutWidth and (b) make the field not editable. These steps enable the text field to determine its intrinsic width and calculate the height needed for autolayout. See this answer and this answer for more details.
Even more obscurely, it follows from the dependency on the text field's editable attribute that autolayout will break if you are using bindings on the field and fail to clear the Conditionally Sets Editable option.

Is there a "right" way to have NSTextFieldCell draw vertically centered text?

I have an NSTableView with several text columns. By default, the dataCell for these columns is an instance of Apple's NSTextFieldCell class, which does all kinds of wonderful things, but it draws text aligned with the top of the cell, and I want the text to be vertically centered in the cell.
There is an internal flag in NSTextFieldCell that can be used to vertically center the text, and it works beautifully. However, since it is an internal flag, its use is not sanctioned by Apple and it could simply disappear without warning in a future release. I am currently using this internal flag because it is simple and effective. Apple has obviously spent some time implementing the feature, so I dislike the idea of re-implementing it.
So; my question is this: What is the right way to implement something that behaves exactly like Apple's NStextFieldCell, but draws vertically centered text instead of top-aligned?
For the record, here is my current "solution":
#interface NSTextFieldCell (MyCategories)
- (void)setVerticalCentering:(BOOL)centerVertical;
#end
#implementation NSTextFieldCell (MyCategories)
- (void)setVerticalCentering:(BOOL)centerVertical
{
#try { _cFlags.vCentered = centerVertical ? 1 : 0; }
#catch(...) { NSLog(#"*** unable to set vertical centering"); }
}
#end
Used as follows:
[[myTableColumn dataCell] setVerticalCentering:YES];
The other answers didn't work for multiple lines. Therefore I initially continued using the undocumented cFlags.vCentered property, but that caused my app to be rejected from the app store. I ended up using a modified version of Matt Bell's solution that works for multiple lines, word wrapping, and a truncated last line:
-(void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSAttributedString *attrString = self.attributedStringValue;
/* if your values can be attributed strings, make them white when selected */
if (self.isHighlighted && self.backgroundStyle==NSBackgroundStyleDark) {
NSMutableAttributedString *whiteString = attrString.mutableCopy;
[whiteString addAttribute: NSForegroundColorAttributeName
value: [NSColor whiteColor]
range: NSMakeRange(0, whiteString.length) ];
attrString = whiteString;
}
[attrString drawWithRect: [self titleRectForBounds:cellFrame]
options: NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin];
}
- (NSRect)titleRectForBounds:(NSRect)theRect {
/* get the standard text content rectangle */
NSRect titleFrame = [super titleRectForBounds:theRect];
/* find out how big the rendered text will be */
NSAttributedString *attrString = self.attributedStringValue;
NSRect textRect = [attrString boundingRectWithSize: titleFrame.size
options: NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin ];
/* If the height of the rendered text is less then the available height,
* we modify the titleRect to center the text vertically */
if (textRect.size.height < titleFrame.size.height) {
titleFrame.origin.y = theRect.origin.y + (theRect.size.height - textRect.size.height) / 2.0;
titleFrame.size.height = textRect.size.height;
}
return titleFrame;
}
(This code assumes ARC; add an autorelease after attrString.mutableCopy if you use manual memory management)
Overriding NSCell's -titleRectForBounds: should do it -- that's the method responsible for telling the cell where to draw its text:
- (NSRect)titleRectForBounds:(NSRect)theRect {
NSRect titleFrame = [super titleRectForBounds:theRect];
NSSize titleSize = [[self attributedStringValue] size];
titleFrame.origin.y = theRect.origin.y + (theRect.size.height - titleSize.height) / 2.0;
return titleFrame;
}
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSRect titleRect = [self titleRectForBounds:cellFrame];
[[self attributedStringValue] drawInRect:titleRect];
}
For anyone attempting this using Matt Ball's drawInteriorWithFrame:inView: method, this will no longer draw a background if you have set your cell to draw one. To solve this add something along the lines of
[[NSColor lightGrayColor] set];
NSRectFill(cellFrame);
to the beginning of your drawInteriorWithFrame:inView: method.
FYI, this works well, although I haven't managed to get it to stay centered when you edit the cell... I sometimes have cells with large amounts of text and this code can result in them being misaligned if the text height is greater then the cell it's trying to vertically center it in. Here's my modified method:
- (NSRect)titleRectForBounds:(NSRect)theRect
{
NSRect titleFrame = [super titleRectForBounds:theRect];
NSSize titleSize = [[self attributedStringValue] size];
// test to see if the text height is bigger then the cell, if it is,
// don't try to center it or it will be pushed up out of the cell!
if ( titleSize.height < theRect.size.height ) {
titleFrame.origin.y = theRect.origin.y + (theRect.size.height - titleSize.height) / 2.0;
}
return titleFrame;
}
No. The right way is to put the Field in another view and use auto layout or that parent view's layout to position it.
Though this is pretty old question...
I believe default style of NSTableView implementation is intended strictly for single line text display with all same size & font.
In that case, I recommend,
Set font.
Adjust rowHeight.
Maybe you will get quietly dense rows. And then, give them padding by setting intercellSpacing.
For example,
core_table_view.rowHeight = [NSFont systemFontSizeForControlSize:(NSSmallControlSize)] + 4;
core_table_view.intercellSpacing = CGSizeMake(10, 80);
Here what you'll get with two property adjustment.
This won't work for multi-line text, but very good enough for quick vertical center if you don't need multi-line support.
I had the same problem and here is the solution I did :
1) In Interface Builder, select your NSTableCellView. Make sure it as big as the row height in the Size Inspector. For example, if your row height is 32, make your Cell height 32
2) Make sure your cell is well placed in your row (I mean visible)
3) Select your TextField inside your Cell and go to your size inspector
4) You should see "Arrange" item and select "Center Vertically in Container"
--> The TextField will center itself in the cell

Resources