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.
Related
I'm trying to understand how to properly display text with condensed line spacing in a text field. When I set paragraph style properties lineHeightMultiple, maximumLineHeight, and minimumLineHeight I can achieve the effect of condensing the lines, but one side effect is that the top line of text just gets clipped off. So I thought that I'd just be able to move the text down with NSBaselineOffsetAttributeName (using a negative value), but that doesn't seem to have any effect. I'm using a line height here of 70% of the point size, but the clipping gets far worse the more condensed it gets.
1) Is there a better way to produce a condensed font line spacing?
2) Or how would you move the text rendering downward so it doesn't get clipped.
<update>
Ok my answer below does address a solution when using NSTextField's. But this obviously doesn't work for NSTextView's too. I tired to override the baselineOffset in the NSLayoutManagerDelegate's shouldSetLineFragmentRect... method, but it also ignores baseline adjustments. Anyone have any suggestions when working with the NSTextView?
</update>
Thanks!
Here's the test project I'm working with https://www.dropbox.com/s/jyshqeuirujf71g/WhatThe.zip?dl=0
Codez:
self.label.wantsLayer = YES;
self.label.backgroundColor = [NSColor whiteColor];
self.label.hidden = NO;
self.label.maximumNumberOfLines = 0;
NSMutableDictionary *result = [NSMutableDictionary dictionary];
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
NSFont *font = [NSFont systemFontOfSize:80.0f];
CGFloat lineHeight = font.pointSize * .7f;
CGFloat natualLineHeight = font.ascender + ABS(font.descender) + font.leading;
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
paragraphStyle.alignment = NSTextAlignmentLeft;
paragraphStyle.lineHeightMultiple = lineHeight / natualLineHeight;
paragraphStyle.maximumLineHeight = lineHeight;
paragraphStyle.minimumLineHeight = lineHeight;
paragraphStyle.paragraphSpacing = 0.0f;
paragraphStyle.allowsDefaultTighteningForTruncation = paragraphStyle.lineBreakMode != NSLineBreakByWordWrapping && paragraphStyle.lineBreakMode != NSLineBreakByCharWrapping && paragraphStyle.lineBreakMode != NSLineBreakByClipping;
result[NSParagraphStyleAttributeName] = paragraphStyle;
result[NSKernAttributeName] = #(0.0f);
result[NSBaselineOffsetAttributeName] = #(-50.0f);
result[NSFontAttributeName] = font;
result[NSForegroundColorAttributeName] = [NSColor blackColor];
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:#"Hello\nThere" attributes:result];
self.label.attributedStringValue = attributedString;
Ok. By subclassing NSTextFieldCell I was able to offset the text correctly. It's a shame that this method works nicely in iOS-land. Maybe this will work when the unified Mac/iOS UI APIs are released this summer. 😁
This will remove any negative baseline values from the string before it draws and draw inside a shifted rect.
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSRect titleRect = [self titleRectForBounds:cellFrame];
NSMutableAttributedString *string = [self.attributedStringValue mutableCopy];
__block CGFloat baselineOffset = 0.0f;
[string enumerateAttributesInRange:NSMakeRange(0, string.length) options:0 usingBlock:^(NSDictionary<NSAttributedStringKey,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
NSNumber *offsetValue = attrs[NSBaselineOffsetAttributeName];
if (offsetValue != nil && offsetValue.floatValue < 0.0f) {
baselineOffset = MIN(baselineOffset, offsetValue.floatValue);
[string removeAttribute:NSBaselineOffsetAttributeName range:range];
}
}];
titleRect.origin.y -= baselineOffset;
[string drawInRect:titleRect];
}
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;
}
I've been struggling with this for quite a few days now; my app has a diagram with uitextfields to represent labelling of the picture. I would like to check the user input against a dictionary (for the answer) and if it is correct, increase the score by 1.
I had it working by 'hard coding' each of the textfield.text queries each with their own if statement, but I would like a better and more reusable way if possible?
I've tried this so far:
- (IBAction)checkAnswers:(UITextField *)textField
{
// array for each textfield
allTextfields = [[NSArray alloc] initWithObjects:eyepiece, objectiveLenses, focussingKnobs, stage, mirror, nil];
// array for each UIImageView
allTicks = [[NSArray alloc] initWithObjects:eyepieceTick, objectiveTick, focussingTick, stageTick, mirrorTick, nil];
UIImage *image = [UIImage imageNamed:#"Tick.png"];
for (textField in allTextfields) {
if ([textField.text isEqualToString:[[microscopeBrain.microscopeDictionary valueForKey:theTextfieldTag] valueForKey:#"Answer"]]) {
[[allTicks objectAtIndex:textField.tag] setImage:image];
x++;
textField.enabled = NO;
NSLog(#"%#", microscopeBrain.microscopeDictionary);
// NSLog(#"%#", [[microscopeBrain.microscopeDictionary valueForKey:theTextfieldTag] valueForKey:#"Answer"]);
}
finalMicroscopeScore = [[NSString alloc] initWithFormat:#"%i", x];
microscopeScoreLabel.text = [[NSString alloc] initWithFormat:#"%i", x];
}
}
The problem is that even if the answers are in the wrong textfield, as long as one is correct, they will all show up as right, which is kind of embarrassing!
Any help would be very much appreciated.
Try changing the valueForKey:theTextFieldTag to valueForKey:textField.tag and see if that helps. You don't show how you get the value for theTextFieldTag, so I'm not sure if that's the problem.
I want to toggle rich text formatting in NSTextView. I have tried following:
[contentView setRichText:NO];
[contentView setImportsGraphics:NO];
but, that didn't changed the NSTextView content and still allowing to do the text formatting.
Please let me know the simple way to toggle/switch rich text formatting in NSTextView just like TextEdit.
I already check the "TextEdit" sample project, but it seems to be very hard to find the usable code from it.
Thanks
Found some help from following link.
click here to see solution
Based on solution given in above link, i have created category for my view controller as follows:
#define TabWidth #"TabWidth"
#interface MyViewController (Helper)
- (NSDictionary *)defaultTextAttributes:(BOOL)forRichText;
- (void)removeAttachments;
- (void)setRichText:(BOOL)flag;
#end
#implementation MyViewController (Helper)
- (NSDictionary *)defaultTextAttributes:(BOOL)forRichText {
static NSParagraphStyle *defaultRichParaStyle = nil;
NSMutableDictionary *textAttributes = [[[NSMutableDictionary alloc] initWithCapacity:2] autorelease];
if (forRichText) {
[textAttributes setObject:[NSFont userFontOfSize:0.0] forKey:NSFontAttributeName];
if (defaultRichParaStyle == nil) { // We do this once...
NSInteger cnt;
NSString *measurementUnits = [[NSUserDefaults standardUserDefaults] objectForKey:#"AppleMeasurementUnits"];
CGFloat tabInterval = ([#"Centimeters" isEqual:measurementUnits]) ? (72.0 / 2.54) : (72.0 / 2.0); // Every cm or half inch
NSMutableParagraphStyle *paraStyle = [[[NSMutableParagraphStyle alloc] init] autorelease];
[paraStyle setTabStops:[NSArray array]]; // This first clears all tab stops
for (cnt = 0; cnt < 12; cnt++) { // Add 12 tab stops, at desired intervals...
NSTextTab *tabStop = [[NSTextTab alloc] initWithType:NSLeftTabStopType location:tabInterval * (cnt + 1)];
[paraStyle addTabStop:tabStop];
[tabStop release];
}
defaultRichParaStyle = [paraStyle copy];
}
[textAttributes setObject:defaultRichParaStyle forKey:NSParagraphStyleAttributeName];
} else {
NSFont *plainFont = [NSFont userFixedPitchFontOfSize:0.0];
NSInteger tabWidth = [[NSUserDefaults standardUserDefaults] integerForKey:TabWidth];
CGFloat charWidth = [#" " sizeWithAttributes:[NSDictionary dictionaryWithObject:plainFont forKey:NSFontAttributeName]].width;
if (charWidth == 0) charWidth = [[plainFont screenFontWithRenderingMode:NSFontDefaultRenderingMode] maximumAdvancement].width;
// Now use a default paragraph style, but with the tab width adjusted
NSMutableParagraphStyle *mStyle = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease];
[mStyle setTabStops:[NSArray array]];
[mStyle setDefaultTabInterval:(charWidth * tabWidth)];
[textAttributes setObject:[[mStyle copy] autorelease] forKey:NSParagraphStyleAttributeName];
// Also set the font
[textAttributes setObject:plainFont forKey:NSFontAttributeName];
}
return textAttributes;
}
/* Used when converting to plain text
*/
- (void)removeAttachments {
NSTextStorage *attrString = [contentView textStorage];
NSUInteger loc = 0;
NSUInteger end = [attrString length];
[attrString beginEditing];
while (loc < end) { /* Run through the string in terms of attachment runs */
NSRange attachmentRange; /* Attachment attribute run */
NSTextAttachment *attachment = [attrString attribute:NSAttachmentAttributeName atIndex:loc longestEffectiveRange:&attachmentRange inRange:NSMakeRange(loc, end-loc)];
if (attachment) { /* If there is an attachment and it is on an attachment character, remove the character */
unichar ch = [[attrString string] characterAtIndex:loc];
if (ch == NSAttachmentCharacter) {
if ([contentView shouldChangeTextInRange:NSMakeRange(loc, 1) replacementString:#""]) {
[attrString replaceCharactersInRange:NSMakeRange(loc, 1) withString:#""];
[contentView didChangeText];
}
end = [attrString length]; /* New length */
}
else loc++; /* Just skip over the current character... */
}
else loc = NSMaxRange(attachmentRange);
}
[attrString endEditing];
}
- (void)setRichText:(BOOL)flag {
NSDictionary *textAttributes;
BOOL isRichText = flag;
if (!isRichText) [self removeAttachments];
[contentView setRichText:isRichText];
[contentView setUsesRuler:isRichText]; /* If NO, this correctly gets rid
of the ruler if it was up */
if (isRichText && NO)
[contentView setRulerVisible:YES]; /* Show ruler if rich, and desired */
[contentView setImportsGraphics:isRichText];
textAttributes = [self defaultTextAttributes:isRichText];
if ([[contentView textStorage] length]) {
[[contentView textStorage] setAttributes:textAttributes range: NSMakeRange(0,[[contentView textStorage] length])];
}
[contentView setTypingAttributes:textAttributes];
}
#end
Where contentView is IBOutlet of NSTextView. Hope this will help someone or let me know if someone has shorter method.
Thanks
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];