NSTextField (multiline) grow horizontally after filling all lines - cocoa

I have multiline NSTexField, that I use to show static text, that may have different length on each showing of window. This NSTextField has a height constrain, that limits it to two lines.
I want to setup behaviour, that NSTextField will grow horizontally only just so much, that it will fill those two horizontal lines.
Now when I set up horizontal compression resistance higher, text field width grows so much to display all the text on one line. Is this possible to achieve only by using autolayout constrains or do I have to calculate nstextfield width somehow?
I tried overloading "layout" method in superview. But its not working as I expect.
- (void)layout
{
[self solveLayoutForView:self];
}
- (void)solveLayoutForView:(NSView*)view
{
for (NSView* subView in [view subviews])
{
if (subView.subviews.count)
[self solveLayoutForView:subView];
else
{
NSLayoutConstraint* height = [subView constraintForAttribute:NSLayoutAttributeHeight];
if (height && height.constant > 17 && [subView isKindOfClass:[NSTextField class]]) // seems like multiline nstextfield
{
NSTextField* textField = (NSTextField*)subView;
textField.preferredMaxLayoutWidth = 0;
[super layout];
textField.preferredMaxLayoutWidth = NSWidth([textField alignmentRectForFrame:textField.frame]);
[super layout];
}
}
}
}

I ended up calculating it by myself.
I overload setStringValue in subclass of NSTextField.
"PreferGrowingHorizontally" is a subclass property if I ever need to skip this calculation (e.g.: in case I would like it to grow horizontally)
- (void)setStringValue:(NSString *)stringValue
{
BOOL changed = ![self.stringValue isEqualToString:stringValue] ? YES : NO;
[super setStringValue:stringValue];
if (stringValue.length && changed)
{
if (self.frame.size.height > 17 && self.preferGrowingHorizontally)
[self compensateWidth];
}
}
- (void)compensateWidth
{
float requiredHeight = FLT_MAX;
int widthCompensation = 0;
while (requiredHeight > self.frame.size.height)
requiredHeight = [self heightForStringDrawing:self.stringValue Font:self.font Width:self.frame.size.width + widthCompensation++];
if (--widthCompensation)
{
NSLayoutConstraint* width = [self constraintForAttribute:NSLayoutAttributeWidth];
if (width)
width.constant = self.frame.size.width + widthCompensation;
NSLog(#"requiredHeight: %f width compensation: %d for textField: %#", requiredHeight, widthCompensation, self.stringValue);
}
}
- (float)heightForStringDrawing:(NSString*)myString Font:(NSFont*)myFont Width:(float)myWidth
{
NSTextStorage *textStorage = [[[NSTextStorage alloc] initWithString:myString] autorelease];
NSTextContainer *textContainer = [[[NSTextContainer alloc] initWithContainerSize:NSMakeSize(myWidth, FLT_MAX)] autorelease];
NSLayoutManager *layoutManager = [[[NSLayoutManager alloc] init] autorelease];
[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];
[textStorage addAttribute:NSFontAttributeName value:myFont
range:NSMakeRange(0, [textStorage length])];
[textContainer setLineFragmentPadding:0.0];
textContainer.lineBreakMode = NSLineBreakByWordWrapping;
(void) [layoutManager glyphRangeForTextContainer:textContainer];
return [layoutManager usedRectForTextContainer:textContainer].size.height;
}

Related

NSView in cocoa refuses to redraw, whatever I try

I'm writing an application on mac to measure colors on screen. For that I have a ColorView that fills a bezierpath with a specific color. When measuring it goes well for most of the patches, but at a given moment the redraw staggers and the view does not get updated anymore. I have been looking for a while now, tried many proposed solutions. None of them are waterproof.
My actual code:
[colorView setColor:[colorsForMeasuring objectAtIndex:index]];
[colorView setNeedsDisplay:YES];
[colorView setNeedsLayout:YES];
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(mainQueue,^{
[colorView setNeedsDisplay:YES];
[colorView setNeedsLayout:YES];
[colorView updateLayer];
[colorView displayIfNeeded];
[colorView display];
});
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
[NSApp runModalSession:aSession];
The drawcode of my colorView is as follows:
- (void)display
{
CALayer *layer = self.layer;
[layer setNeedsDisplay];
[layer displayIfNeeded];
}
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context
{
[self internalDrawWithRect:self.bounds];
}
- (void)internalDrawWithRect:(CGRect)rect
{
NSRect bounds = [self bounds];
[color set];
[NSBezierPath fillRect:bounds];
}
- (void)drawRect:(CGRect)rect {
[self internalDrawWithRect:rect];
}
Any help would be very welcome! My original code was much simpler, but I kept on adding things that might have helped.
As I said, the original code was rather straightfoward and simple:
The measuring loop:
for ( uint32 index = 0; index < numberOfColors && !stopMeasuring; index++ )
{
#ifndef NDEBUG
NSLog( #"Color on screen:%#", [colorsForMeasuring objectAtIndex:index] );
#endif
[colorView setColor:[colorsForMeasuring objectAtIndex:index]];
[colorView setNeedsDisplay:YES];
[colorView displayIfNeeded];
// Measure the displayed color in XYZ values
CGFloat myRed, myGreen, myBlue, myAlpha;
[[colorsForMeasuring objectAtIndex:index] getRed:&myRed green:&myGreen blue:&myBlue alpha:&myAlpha];
float theR = (float) myRed;
float theG = (float) myGreen;
float theB = (float) myBlue;
xyzData = [measDeviceController measureOnceXYZforRed:255.0f*theR Green:255.0f*theG Blue:255.0f*theB];
if ( [xyzData count] > 0 )
{
measuringResults[index*3] = [[xyzData objectAtIndex:0] floatValue];
measuringResults[(index*3) + 1] = [[xyzData objectAtIndex:1] floatValue];
measuringResults[(index*3) + 2] = [[xyzData objectAtIndex:2] floatValue];
#ifndef NDEBUG
printf("Measured value X=%f, Y=%f, Z=%f\n", measuringResults[index*3], measuringResults[(index*3) + 1], measuringResults[(index*3) + 2]);
#endif
} }
The drawing of the MCTD_ColorView which just is inherited from NSView:
- (void)setColor:(NSColor *)aColor;
{
[aColor retain];
[color release];
color = aColor;
}
- (void)drawRect:(NSRect)rect
{
NSRect bounds = [self bounds];
[color set];
[NSBezierPath fillRect:bounds];
}
- (void)dealloc
{
[color release];
[super dealloc];
}
When running my measurement loop and after putting breakpoints in "setColor" and "drawRect", debugging always stops in setColor but never in drawRect. Without debugging, the view remains white while I should see all different colors appearing.
As can be seen in my first post, I have tried many things to get it drawing, but they all failed.

NSTextView double vision

I must be doing something wrong here:
My Cocoa app has a scrollview around a custom view which in turn has a textview. I only expect to see one "This is a " string but there the extra one up in the corner.
I have reduced the code to something very minimal and still do not understand what my error is, so here I am fishing for a clue.
The view controller for the custom view follows, but for simplicity here is a link to the project.
#import "TTTSimpleCtrlView.h"
#interface TTTSimpleCtrlView ()
#property (strong,nonatomic) NSTextView *tv1;
#property (strong,nonatomic) NSTextStorage *ts;
#end
#implementation TTTSimpleCtrlView
- (void) awakeFromNib {
NSFont *font = [NSFont fontWithName:#"Courier New Bold" size:20.0f];
NSMutableParagraphStyle *styleModel = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
[styleModel setLineHeightMultiple:1.0];
// [styleModel setLineSpacing:fontRect.size.height * 2];
NSDictionary *textAttrs = [NSDictionary dictionaryWithObjectsAndKeys: font, NSFontAttributeName,
[NSColor blackColor] ,NSForegroundColorAttributeName,
[NSColor whiteColor], NSBackgroundColorAttributeName,
styleModel, NSParagraphStyleAttributeName,
nil];
NSString *pilcrowStr = #"This is a test.";
NSAttributedString *s = [[NSAttributedString alloc] initWithString:pilcrowStr attributes:textAttrs];
NSRect rect = [s boundingRectWithSize:NSMakeSize(INFINITY,INFINITY)options:0];
NSLayoutManager *lm = [[NSLayoutManager alloc] init];
NSTextContainer *tc = [NSTextContainer new];
[tc setContainerSize:s.size];
[lm addTextContainer:tc];
_ts = [[NSTextStorage alloc] init];
[_ts setAttributedString:s];
[_ts addLayoutManager:lm];
[lm replaceTextStorage:_ts];
rect.origin.x = 10;
rect.origin.y = rect.size.height;
NSTextView *v = [[NSTextView alloc] initWithFrame:rect textContainer:tc];
[v setDrawsBackground:YES];
[self addSubview:v];
}
- (BOOL) isFlipped {
return YES;
}
- (void)drawRect:(NSRect)rect
{
NSLog(#"drawRect & %lu subviews",self.subviews.count);
for (NSTextView *v in self.subviews) {
if(CGRectIntersectsRect(v.frame, rect) || CGRectContainsRect(rect, v.frame)) {
[v drawRect:rect];
NSLog(#"frame = %#",NSStringFromRect(v.frame));
}
}
[super drawRect:rect];
}
You are calling:
[super drawRect:rect];
and you are drawing the text yourself in your draw function.
In effect you are drawing the text and cocoa is drawing the text for you as well.
So don't call super.

How to calculate correct coordinates for selected text in NSTextView?

I need to highlight line with caret in NSTextView using CALayer overlay and grab rect for line with this code in my subclassed NSTextView...:
- (NSRect)overlayRectForRange:(NSRange)aRange
{
NSScreen *currentScreen = [NSScreen currentScreenForMouseLocation];
NSRect rect = [self firstRectForCharacterRange:aRange];
rect = [self convertRectToLayer:rect];
rect.origin = [currentScreen flipPoint:rect.origin];
rect = [self.window convertRectToScreen:rect];
return rect;
}
...and put overlay:
- (void)focusOnLine
{
NSInteger caretLocation = [aTextView selectedRange].location;
NSRange neededRange;
(void)[layoutMgr lineFragmentRectForGlyphAtIndex:caretLocation effectiveRange:&neededRange];
CALayer *aLayer = [CALayer layer];
[aLayer setFrame:NSRectToCGRect([aTextView overlayRectForRange:neededRange])];
[aLayer setBackgroundColor:[[NSColor redColor] coreGraphicsColorWithAlfa:0.5]];
[[aTextView layer] addSublayer:aLayer];
}
As a result, the selection overlay coincides with width of desired line, but absolutely not matches by Y axis (X axis is ok).
What am I missing?
Okay, I completely rewrote method overlayRectForRange: and now all works fine. There is fixed code:
- (NSRect)overlayRectForRange:(NSRange)aRange
{
NSRange activeRange = [[self layoutManager] glyphRangeForCharacterRange:aRange actualCharacterRange:NULL];
NSRect neededRect = [[self layoutManager] boundingRectForGlyphRange:activeRange inTextContainer:[self textContainer]];
NSPoint containerOrigin = [self textContainerOrigin];
neededRect.origin.x += containerOrigin.x;
neededRect.origin.y += containerOrigin.y;
neededRect = [self convertRectToLayer:neededRect];
return neededRect;
}

How to optimally fit a NSString in a rectangle? [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Change NSTextField font size to fit
I am trying to fit a string of variable length (the number of words in the string is unknown) inside a given rectangle. I want to optimally size the string so that it is as big as possible and fits inside the rectangle. Further more, the string should word wrap if there is more than one word and that a word should not be partially rendered on multiple lines. My problem is sometimes a word is partially laid out on multiple lines as seen below. Any suggestions on what I might be doing wrong?
Thank you.
I am using an NSLayoutManager, NSTextStorage and NSTextContainer.
I initialize everything as follows:
textStorage = [[NSTextStorage alloc] initWithString:#""];
layoutManager = [[NSLayoutManager alloc] init];
textContainer = [[NSTextContainer alloc] init];
[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];
paraStyle = [[NSMutableParagraphStyle alloc] init];
[paraStyle setLineBreakMode:NSLineBreakByWordWrapping];
[paraStyle setParagraphStyle:[NSParagraphStyle defaultParagraphStyle]];
[paraStyle setAlignment:NSCenterTextAlignment];
I then compute the font size as follows,
- (float)calculateFontSizeForString:(NSString *)aString andBoxSize:(NSSize)aBox
{
//Create the attributed string
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:aString];
[textStorage setAttributedString:attrString];
[textContainer setContainerSize:NSMakeSize(aBox.width, FLT_MAX)];
[attrString release]; //Clean up
//Initial values
float fontSize = 50.0;
float fontStepSize = 100.0;
NSRect stringRect;
BOOL didFindHeight = NO;
BOOL shouldIncreaseHeight = YES;
while (!didFindHeight)
{
NSMutableDictionary *stringAttributes = [NSMutableDictionary dictionaryWithObjectsAndKeys:
paraStyle, NSParagraphStyleAttributeName,
[NSFont systemFontOfSize:fontSize], NSFontAttributeName, nil];
[textStorage addAttributes:stringAttributes range:NSMakeRange(0, [textStorage length])];
(void)[layoutManager glyphRangeForTextContainer:textContainer];
stringRect = [layoutManager usedRectForTextContainer:textContainer];
if (shouldIncreaseHeight)
{
if (stringRect.size.height > aBox.height)
{
shouldIncreaseHeight = NO;
fontStepSize = fontStepSize/2;
}
fontSize += fontStepSize;
}
else
{
if (stringRect.size.height < aBox.height)
{
shouldIncreaseHeight = YES;
fontStepSize = fontStepSize/2;
if (fontStepSize <= 0.5)
{
didFindHeight = YES;
}
}
if ((fontSize - fontStepSize) <= 0)
{
fontStepSize = fontStepSize/2;
}
else
{
fontSize -= fontStepSize;
}
}
}
return fontSize;
}
Please search before posting. This comes up repeatedly. Latest answer is here, but I think there're more complete answers with code listings elsewhere.
My admittedly simple example shows how to do it without a text container and layout manager but your approach is more robust. Unfortunately brute-force (sizing down until it fits) is the only approach for determining the best fit.

How to resize NSTextView according to its content?

I am trying to set an attributed string within NSTextView. I want to increase its height based on its content, initially it is set to some default value.
So I tried this method:
I set content in NSTextView. When we set some content in NSTextView its size automatically increases. So I increased height of its super view that is NSScrollView to its height, but NSScrollView is not getting completely resized, it is showing scroller on right.
float xCoordinate = 15.0;
[xContentViewScroller setFrame:NSMakeRect(xCoordinate, 0.0, 560.0, 10.0)];
[[xContentView textStorage] setAttributedString:xContents];
float xContentViewScrollerHeight = [xfContentView frame].size.height + 2;
[xContentViewScroller setFrame:NSMakeRect(xCoordinate, 0.0, 560.0, xContentViewScrollerHeight)];
Can anyone suggest me some way or method to resolve this issue. By doing google search I found that in UITextView there is contentSize method which can get size of its content, I tried to find similar method in NSTextView but could not get any success :(
Based on #Peter Hosey's answer, here is an extension to NSTextView in Swift 4.2:
extension NSTextView {
var contentSize: CGSize {
get {
guard let layoutManager = layoutManager, let textContainer = textContainer else {
print("textView no layoutManager or textContainer")
return .zero
}
layoutManager.ensureLayout(for: textContainer)
return layoutManager.usedRect(for: textContainer).size
}
}
}
NSTextView *textView = [[NSTextView alloc] init];
textView.font = [NSFont systemFontOfSize:[NSFont systemFontSize]];
textView.string = #"Lorem ipsum";
[textView.layoutManager ensureLayoutForTextContainer:textView.textContainer];
textView.frame = [textView.layoutManager usedRectForTextContainer:textView.textContainer];
+ (float)heightForString:(NSString *)myString font:(NSFont *)myFont andWidth:(float)myWidth andPadding:(float)padding {
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithString:myString];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithContainerSize:NSMakeSize(myWidth, FLT_MAX)];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];
[textStorage addAttribute:NSFontAttributeName value:myFont
range:NSMakeRange(0, textStorage.length)];
textContainer.lineFragmentPadding = padding;
(void) [layoutManager glyphRangeForTextContainer:textContainer];
return [layoutManager usedRectForTextContainer:textContainer].size.height;
}
I did the function using this reference: Documentation
Example:
float width = textView.frame.size.width - 2 * textView.textContainerInset.width;
float proposedHeight = [Utils heightForString:textView.string font:textView.font andWidth:width
andPadding:textView.textContainer.lineFragmentPadding];

Resources