Creating right aligned NSView - macos

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)-.

Related

setStringValue in NSTextField will always change height

I have a NSTextField in a view where layout is totally controlled by constraints and translatesAutoresizingMaskIntoConstraints is NO. I tried to use setStringValue to change the content like this:
[[self textfield] setStringValue:#"1\n2\n3\n"];
Then the height is changed to 4 lines which is not what I want. I need a NSTextField that can show only one line but still I can use up and down arrow keys to go into different lines. It is just like use option+enter to insert a newline in NSTextField.
I also tried to keep the height:
NSRect originalFrame = [[self textfield] frame];
[[self textfield] setStringValue:#"1\n2\n3\n"];
NSRect newFrame = [[self textfield] frame];
newFrame.size.height = originalFrame.size.height;
[[self textfield] setFrame:newFrame];
It doesn't work. I checked intrinsicContentSize and it returns (width=-1, height=73). Is there anything I can set to NSTextField so the height is of only one line like 22?
This is happening because you have set the auto layout of textfield with respect to the view. So just fixed the height of your textfield by clicking on the height checkbox like that below :-

NSTextField width and autolayout

I am trying to create an NSTextField programmatically.
I want to use this NSTextField with auto layout, so its width will be defined automatically to display the entire line (there is only one line of text).
The problem is that textField.intrinsicContentSize and textField.fittingSize are both have -1 and 0 values for the horizontal coordinate, as the output of the code below is:
textField.intrinsicContentSize={-1, 21}
textField.fittingSize={0, 21}
The code:
NSTextField* textField = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 10, 24)];
NSString* str = #"One line of text here.";
[textField setStringValue: str];
[textField setTranslatesAutoresizingMaskIntoConstraints:NO];
[textField invalidateIntrinsicContentSize];
NSLog(#"textField.intrinsicContentSize=%#", NSStringFromSize(textField.intrinsicContentSize));
NSLog(#"textField.fittingSize=%#", NSStringFromSize(textField.fittingSize));
This makes the text field to have a zero width assigned by auto layout.
What should I do to get meaningful values for the fittingSize and intrinsicContentSize properties of the text field so they reflect the content of the text field?
Another way to compute the text field optimal width without using fittingSize is using the following code (replacing John Sauer's sizeTextFieldWidthToFit method):
- (void) sizeTextFieldWidthToFit {
[textField sizeToFit];
CGFloat newWidth = NSWidth(textField.frame);
textFieldWidthConstraint.constant = newWidth;
}
You can set the priority of textFieldWidthConstraint to be lower than the priority of another inequality constrain the represent your requirement to a minimal size of 25.
I can't explain why NSTextField's intrinsicContentSize's widths are -1, but I was able to calculate one based off its attributedString's size. Here's how I created the NSTextField:
// textField is an instance variable.
textField = [NSTextField new] ;
textField.translatesAutoresizingMaskIntoConstraints = NO ;
[view addSubview:textField] ;
NSDictionary* viewsDict = NSDictionaryOfVariableBindings(view, textField) ;
// textFieldWidthConstraint is an instance variable.
textFieldWidthConstraint = [NSLayoutConstraint constraintsWithVisualFormat:#"H:[textField(==0)]" options:0 metrics:nil views:viewsDict] [0] ;
[view addConstraint:textFieldWidthConstraint] ;
NSNumber* intrinsicHeight = #( textField.intrinsicContentSize.height ) ;
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[textField(==intrinsicHeight)]" options:0 metrics:NSDictionaryOfVariableBindings(intrinsicHeight) views:viewsDict]] ;
// Position textField however you like.
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[textField]" options:0 metrics:nil views:viewsDict]] ;
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[textField]" options:0 metrics:nil views:viewsDict]] ;
[self sizeTextFieldWidthToFit] ;
// set textField's delegate so that sizeTextFieldWidthToFit is called when textField's text changes.
textField.delegate = self ;
And here are the methods that resize it as its text changes:
- (void) controlTextDidChange:(NSNotification*)aNotification {
if ( aNotification.object == textField )
[self sizeTextFieldWidthToFit] ;
}
- (void) sizeTextFieldWidthToFit {
// I'd like the width to always be >= 25.
CGFloat newWidth = 25 ;
if ( ! [textField.stringValue isEqualToString:#""] ) {
NSDictionary* attributes = [textField.attributedStringValue attributesAtIndex:0 effectiveRange:nil] ;
NSSize size = [textField.stringValue sizeWithAttributes:attributes] ;
newWidth = MAX(newWidth, size.width + 10) ;
}
textFieldWidthConstraint.constant = newWidth ;
}
I'm adding this here because these answers are old and I couldn't find everything needed to get NSTextField to automatically wrap and play nice using autolayout and NSConstraints in a single answer.
With these settings the text will:
Remain in the horizontal boundaries set by the constraints.
Wrap automatically on word boundaries.
Automatically adjust its height as required.
Push down anything below (if attached by constraints).
This is the code to make it just work:
NSTextField *instructions = [NSTextField labelWithString:#"Some very long text..."];
instructions.translatesAutoresizingMaskIntoConstraints = NO;
instructions.autoresizesSubviews = YES;
instructions.usesSingleLineMode = NO;
instructions.lineBreakMode = NSLineBreakByWordWrapping;
[instructions cell].wraps = YES;
[instructions cell].scrollable = NO;
instructions.maximumNumberOfLines = 10;
instructions.preferredMaxLayoutWidth = 400.0;
[self addSubview:iCloudInstructions];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[instructions]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(instructions)]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[instructions]-(20)-[button]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(instructions, instructions)]];

cocoa - add NSTextfield to NSRect

I'm confused as to why the code below isn't working, what I would like to achieve is to have a NSTextfield in a NSRect but I'm not sure if it's possible and if it is how to do it, I tried the code below but it's not working...
NSRect city_label = NSMakeRect(20, 20, 7, 7);
NSTextField *label = [[NSTextField alloc] initWithFrame:city_label];
label.stringValue = #"Contents of NSTextfield";
The NSRect gets drawn in an NSView
Anyone any ideas?
An NSRect is not the sort of entity which could be "drawn in an NSView"--it is not an instance of a subview of NSView. An NSRect is just a C struct describe size (width and height) and origin (x and y).
After initializing your NSTextField with its frame (keep in mind that the origin here is relative to the view to which you will add the text field as a subview), you must add it to the view that you want to have as its superview. Assuming for a moment that we're in a custom subclass of NSViewController, your code just needs this additional line
[self.view addSubview:label];

View-backed NSTableView: Inserted Column Width/Margin Issues

I have a view-based NSTableView that usually has one column in it. However, at a button press, I want a new column to slide in from the left (very similar to what happens on an iPhone when you click the Edit button in Mail). For now, the view that I want to slide is in a very simple view that draws a solid background: its drawRect: just does
[[NSColor blueColor] set];
[[NSBezierPath bezierPathWithRect:[self bounds]] fill];
In the delegate for my NSTableView, I have the following:
- (IBAction)buttonPressed:(id)sender
{
NSTableColumn *newColumn = [[NSTableColumn alloc] initWithIdentifier:#"InPreviewColumn"];
[newColumn setWidth:40];
[newColumn setMinWidth:[newColumn width]];
/* To make up for there not being an insertColumnAt: method,
hide the column, add it, and move it to the front before showing it. */
[newColumn setHidden:YES];
[availableFontsView beginUpdates];
[availableFontsView addTableColumn:newColumn];
[availableFontsView moveColumn:1 toColumn:0];
[availableFontsView endUpdates];
[newColumn setHidden:NO];
}
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
/* ... code for main column ... */
else if([[tableColumn identifier] isEqualToString:#"InPreviewColumn"])
{
USSolidBackgroundView *v = [tableView makeViewWithIdentifier:[tableColumn identifier] owner:self];
if(!v)
{
v = [[[USSolidBackgroundView alloc] initWithFrame:NSMakeRect(0, 0, [tableColumn width], 0)] autorelease];
[v setIdentifier:[tableColumn identifier]];
[v setAutoresizingMask:NSViewMinXMargin | NSViewWidthSizable | NSViewMaxXMargin];
}
return v;
}
}
Yet, when I do this, I end up with this result (there's a big non-blue margin between the blue part of the new column and the start of the main column):
When the additional column isn't present, there's no margin so I'm pretty sure that the problem isn't with the other view (since there's no margin when it's the only view displayed).
I've used logging statements to verify that solid color view's bounds always has a width of 40 (in its drawRect:) and, at least when the view is created, the table column has a width of 40 as well.
So where does this margin come from? No matter how I size the column, it seem that only roughly half of it is blue. So, the bigger the column, the bigger the margin.
How do I make the entire extra column blue?
The issue was my being stupid: In the view that draws the font names, I was basing the position on [self frame]. Which is wrong.
[self bounds] is the way to go. Which I knew. Can't believe I made this mistake. Amateur hour.
If you wander by this question, feel free to vote to close it or flag it or whatever it is that's supposed to work on Stackoverflow.
My apologies.

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