NSLayoutManager/NSTextContainer Ignores Scale Factor - macos

I am attempting to measure an NSAttributedString's height given a constant width using the following method:
-(CGFloat)calculateHeightForAttributedString:(NSAttributedString*)attributedNotes {
CGFloat scrollerWidth = [NSScroller scrollerWidthForControlSize:NSRegularControlSize scrollerStyle:NSScrollerStyleLegacy];
CGFloat width = self.tableView.frame.size.width - self.cellNotesWidthConstraint - scrollerWidth;
// http://www.cocoabuilder.com/archive/cocoa/54083-height-of-string-with-fixed-width-and-given-font.html
NSTextView *tv = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, width - 20, 1e7)];
tv.font = [NSFont userFontOfSize:32];
[tv.textStorage setAttributedString:attributedNotes];
[self setScaleFactor:[self convertSliderValueToFontScale:self.fontScaleSlider] forTextView:tv];
[tv.layoutManager glyphRangeForTextContainer:tv.textContainer];
[tv.layoutManager ensureLayoutForTextContainer:tv.textContainer];
return [tv.layoutManager usedRectForTextContainer:tv.textContainer].size.height + 10.0f; // add a little bit of a buffer
}
Basically, the width is the table view's size minus the scroller and a little bit of each cell that is used to display other information. This method works really well as long as the text scale (via convertSliderValueToFontScale:) is 1.0. The result from usedRectForTextContainer is incorrect if I change the scale factor, however -- as if the scale factor was not being accounted for.
The scale is set in setScaleFactor:forTextView: on the NSTextView as follows (scalar is the actual scale amount):
[textView scaleUnitSquareToSize:NSMakeSize(scaler, scaler)];
Any ideas on how to fix this?
Edit: I've got a sample project to try here: Github. Strangely enough, things work if the scale is < 0, and they randomly seem to work in the 4.XXX range on occasion...

The answer turns out to be simple: add the NSTextView as a subview of an NSClipView with the same frame as the NSTextView.
The final height function is as follows:
-(CGFloat)calculateHeightForAttributedString:(NSAttributedString*)attributedNotes {
CGFloat width = self.textView.frame.size.width;
// http://www.cocoabuilder.com/archive/cocoa/54083-height-of-string-with-fixed-width-and-given-font.html
NSTextView *tv = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, width, 1e7)];
tv.horizontallyResizable = NO;
tv.font = [NSFont userFontOfSize:32];
tv.alignment = NSTextAlignmentLeft;
[tv.textStorage setAttributedString:attributedNotes];
[self setScaleFactor:self.slider.floatValue forTextView:tv];
// In order for usedRectForTextContainer: to be accurate with a scale, you MUST
// set the NSTextView as a subview of an NSClipView!
NSClipView *clipView = [[NSClipView alloc] initWithFrame:NSMakeRect(0, 0, width, 1e7)];
[clipView addSubview:tv];
[tv.layoutManager glyphRangeForTextContainer:tv.textContainer];
[tv.layoutManager ensureLayoutForTextContainer:tv.textContainer];
return [tv.layoutManager usedRectForTextContainer:tv.textContainer].size.height;
}

Related

Updating the size of NSTextField with respect to length of the string

I am trying to update the height of the NSTextField w.r.t the length of the string but if the string is light the last portion gets cut off.This is my code.
self.solutionTextView.frame = CGRectMake(self.solutionTextView.frame.origin.x, self.solutionTextView.frame.origin.y, self.solutionTextView.frame.size.width + 25 ,self.solutionTextView.frame.size.height + 25);
//self.solutionTextView.sizeToFit()
What am i doing wrong?
If the NSTextField is allowed to wrap the string, the following steps are the standard (or usual? or one of more possible?) ways to determine the wanted height of the field. Assume the width of the field is given:
NSRect frame = [textField frame];
Now make it very high:
NSRect constraintBounds = frame;
constraintBounds.size.height = 10000.0;
[textField setFrame: constraintBounds];
and set the string:
[textField setStringValue:theString];
Ask now for the natural size:
NSSize naturalSize =
[[textField cell] cellSizeForBounds:constraintBounds];
And you are done:
frame.size = naturalSize;
[textField setFrame:frame];

Creating right aligned NSView

I want to create a NSView container such that any NSControl object added should be right aligned.
I have added a method to MyCustomNSView class as following. Currently I am adding buttons which are getting left aligned.
- (void) _addButton:(NSString *)title withIdentifier:(NSString *)identifier {
NSButton *button = [[NSButton alloc] initWithFrame:NSMakeRect(100 * [_buttonIdentifierList count] + 10 , 5, 70, 20)];
[button setTitle:title];
[button setAction:#selector(actionButtonPressed:)];
[button setTarget:self];
[button setIdentifier:identifier];
[self addSubview:button];
[_buttonIdentifierList addObject:identifier];
}
So what modifications do I have to make to the above method so that it will add the objects from right side.
I was planning to do it mathematically(Generating frame origin that would generate right aligned origin point). I also tried out using NSLayoutConstrains but didnt work out..
How do I do it using autolayouts ?
To do it by manual positioning, you would compute the frame for the button something like this:
NSButton *button = [[NSButton alloc] initWithFrame:NSMakeRect(NSMaxX(self.bounds) - (100 * [_buttonIdentifierList count] + 10) - 70, 5, 70, 20)];
That is, you take your current calculation which is an offset toward the right (from the left edge) and negate it to make it an offset toward the left. You add the value of the right edge of the containing view so it's an offset from the right edge. That has computed the X position of the right edge of the button, so you subtract the button's width to get the origin of the button, which is on its left edge.
To use auto layout (which uses NSLayoutConstraint), you could do this:
NSButton *button = [[NSButton alloc] initWithFrame:NSZeroRect];
[button setTitle:title];
[button setAction:#selector(actionButtonPressed:)];
[button setTarget:self];
[button setIdentifier:identifier];
button.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:button];
__block NSButton* previousButton = nil;
if (_buttonIdentifierList.count)
{
NSString* previousButtonIdentifier = _buttonIdentifierList.lastObject;
[self.subviews enumerateObjectsUsingBlock:^(NSView* subview, NSUInteger idx, BOOL *stop){
if ([subview.identifier isEqualToString:previousButtonIdentifier])
{
previousButton = (NSButton*)subview;
*stop = YES;
}
}];
}
NSDictionary* metrics = #{ #"buttonWidth": #70,
#"buttonHeight": #20,
#"buttonSeparation": #30,
#"horizontalMargin": #10,
#"verticalMargin": #5 };
if (previousButton)
{
NSDictionary* views = NSDictionaryOfVariableBindings(button, previousButton);
NSArray* constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"[button(buttonWidth)]-(buttonSeparation)-[previousButton]" options:NSLayoutFormatAlignAllBaseline metrics:metrics views:views];
[self addConstraints:constraints];
}
else
{
NSDictionary* views = NSDictionaryOfVariableBindings(button);
NSArray* constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"[button(buttonWidth)]-(horizontalMargin)-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[button(buttonHeight)]-(verticalMargin)-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
}
[_buttonIdentifierList addObject:identifier];
Finding the previousButton would be simplified if you keep track of the buttons, rather than the identifiers. If you have a button object, it's easy to get its identifier, but the reverse (getting the button object when all you have is the identifier) is not as simple.
If you want to allow the buttons to be their natural width and height, rather than a fixed value, you can just leave out those width/height specifiers (that is, use [button] rather than [button(buttonWidth)]). If you want all of the buttons to have the same width, but let the system pick the width of the naturally widest button, you can use [button(==previousButton)]. Since a button's default compression resistance priority is higher than its content hugging priority, it will pick the smallest width that doesn't compress any of them.
If you want the buttons to be the standard distance away from each other, rather than the fixed value of 30 points, you can use use - instead of -(buttonSeparation)-. Similarly, if you want them to be the standard distance from the superview edge, you can use - instead of -(horizontalMargin)- or -(verticalMargin)-.

Centering UIImage and UILabel together

I want to center my label's text while also considering a 20x20 image that should always be just to the left of the label. Specifically, I have three labels who's text could be anything, and I want a check mark to always appear just to the left of each label - this position will vary depending on the text length though.
My best guess is making the image a subview of the label and then indenting the text but that still will be somewhat inconsistent...
In iOS 6 you can do this with constraints and almost no code. But for iOS 5 using only springs and struts you need to manage the view frames your self. You will also need to call sizeToFit so the UILabel resizes it's self to the current text.
Here is some example code:
- (void)centerLabels
{
CGRect viewFrame = self.view.frame;
NSArray *allLabelsAndChecks = #[ #[self.label1, self.check1], #[self.label2, self.check2], #[self.label3, self.check3] ];
for (NSArray *labelAndCheck in allLabelsAndChecks) {
UILabel *label = labelAndCheck[0];
UIImageView *check = labelAndCheck[1];
[label sizeToFit];
CGRect labelFrame = label.frame;
CGRect checkFrame = check.frame;
CGFloat maxWidth = viewFrame.size.width - (leftMarginText + rightMargin);
if (labelFrame.size.width > maxWidth) {
labelFrame.origin.x = leftMarginText;
labelFrame.size.width = maxWidth;
checkFrame.origin.x = leftMarginImage;
} else {
CGFloat slideRight = (maxWidth - labelFrame.size.width) / 2.0;
labelFrame.origin.x = leftMarginText + slideRight;
checkFrame.origin.x = leftMarginImage + slideRight;
}
label.frame = labelFrame;
check.frame = checkFrame;
}
}
The commplete demo project can be found here: https://github.com/GayleDDS/TestCenteredLabel.git

How to draw both stroked and filled text in drawLayer:inContext delegate

this is my drawLayer method in a CALayer's delegate.
it's only responsible for drawing a string with length = 1.
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
CGRect boundingBox = CGContextGetClipBoundingBox(ctx);
NSAttributedString *string = [[NSAttributedString alloc] initWithString:self.letter attributes:[self attrs]];
CGContextSaveGState(ctx);
CGContextSetShadowWithColor(ctx, CGSizeZero, 3.0, CGColorCreateGenericRGB(1.0, 1.0, 0.922, 1.0));
CTLineRef line = CTLineCreateWithAttributedString((CFAttributedStringRef)string);
CGRect rect = CTLineGetImageBounds(line, ctx);
CGFloat xOffset = CGRectGetMidX(rect);
CGFloat yOffset = CGRectGetMidY(rect);
CGPoint pos = CGPointMake(CGRectGetMidX(boundingBox) - xOffset, CGRectGetMidY(boundingBox)- yOffset);
CGContextSetTextPosition(ctx, pos.x, pos.y);
CTLineDraw(line, ctx);
CGContextRestoreGState(ctx);
}
here's the attributes dictionary:
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
[NSFont fontWithName:#"GillSans-Bold" size:72.0], NSFontAttributeName,
[NSColor blackColor], NSForegroundColorAttributeName,
[NSNumber numberWithFloat:1.0], NSStrokeWidthAttributeName,
[NSColor blackColor], NSStrokeColorAttributeName,
style, NSParagraphStyleAttributeName, nil];
as is, the stroke does not draw, but the fill does.
if i comment out the stroke attributes in the dictionary, the fill draws.
i know this can't be right, but i can't find any reference to this problem.
is this a known issue when drawing text with a delegate ?
as the string is one character, i was following the doc example not using any framesetter machinery, but tried that anyway as a fix attempt without success.
in reading this question's answer, i realized that i needed to be using a negative number for the stroke value. i was thinking of the stroke being applied to the outside of the letter drawn by CTLineDraw, rather then inside the text shape.
i'm answering my own question, in case this should help anyone else with this misunderstanding, as i didn't see the referenced doc covering this.

removing/adding CALayers for GPU optimization

I have a layer backed view, I am trying to add subLayers roughly sized around 300 X 270 (in pixels) to it.
The sublayers' count may reach 1000 to 2000, not to mention each sublayer is again scalable to roughly 4280 X 1500 or more for starters.
So the problem is obviously that of a GPU constraint.
After adding around 100 subLayers sized 300 X 270 , there is a warning image is too large for GPU, ignoring and that is messing with the layer display.
The solution for such a problem (from some mailing lists) was to use CATiledLayer, but I can't make use of the tiledLayer due to the complex requirement of the subLayers' display.
Is there a possibility of removing the subLayers which don't fall under VisibleRect of the view?
I tried to removeFromSuperlayer and then add it whenever required, there's always a crash when I try to add the subLayer back.
How can I do this?
I am adding sublayer twice (I need to change it) but for now just for the gist of the code:
-(IBAction)addLayer:(id)sender
{
Layer *l = [[Layer alloc] init];
CALayer *layer = [l page];
[contentArray addObject:page];
[drawLayer addSublayer:layer];
[self layout];
}
-(void)layout
{
NSEnumerator *pageEnumr = [contentArray objectEnumerator];
float widthMargin = [self frame].size.width;
CGRect rect;
float zoom = [self zoomFactor];
while(obj = [contentEnmr nextObject] )
{
[obj setZoomFactor:zoom];
CALayer *pg =(CALayer *)[obj page] ;
rect = pg.bounds;
if ( x + pg.bounds.size.width > widthMargin )
{
x = xOffset;
y += rect.size.height + spacing ;
}
rect.origin = CGPointMake(x,y);
[obj changeBounds];
NSRect VisibleRect = [self visibleRect];
NSRect result = NSIntersectionRect(VisibleRect,NSRectFromCGRect( rect));
if( NSEqualRects (result ,NSZeroRect) )
{
[pg removeFromSuperlayer];
}else
{
[drawLayer addSublayer:pg];
[pg setFrame:rect];
[pg setNeedsDisplay];
}
x += ( rect.size.width + spacing);
}
NSRect viewRect = [self frame];
if(viewRect.size.height < ( y + rect.size.height + spacing ) )
viewRect.size.height = ( y + rect.size.height + spacing) ;
[self setFrameSize: viewRect.size];
}
#interface Layer : NSObject {
CALayer *page;
}
#property (retain) CALayer *page;
Have a look at the PhotoScroller application included as part of the WWDC conference. It demonstrates how to zoom and scroll through a very large image by loading only portions of that image that are currently visible.
Also check out this discussion.
You'll need to do what NSTableView and UITableView do, and manage the addition / removal of layers yourself whenever the visible rect changes. Subscribe to the boundsDidChange noitification of the enclosing scroll view's clip view (I'm assuming that the reason some of the layer is offscreen is that it's enclosed in a scroll view):
- (void) viewDidMoveToSuperview
{
NSClipView* clipView = [[self enclosingScrollView] contentView];
[clipView setPostsBoundsChangedNotifications:YES];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(clipViewBoundsDidChange:)
name:NSViewBoundsDidChangeNotification
object:clipView];
}
and then write a clipViewBoundsDidChange: method that adds and removes sublayers as needed. You may also want to cache and reuse invalidated layers to cut down on allocations. Take a look at the way UITableView and NSTableView interact with their dataSource object for some ideas about how to design the interface for this.
CATiledLayer solves this problem the content of a layer --- ie, whatever you set its contents property or draw into its graphics context directly. It won't do this for sublayers, in fact I think you're advised not to add sublayers to a CATiledLayer at all, as this interferes with its drawing behaviour.

Resources