NSShadow drawing bug - macos

Hi in my application i have a custom view with CALayer (i use it for transition animation), in that view i add some subviews, one of them is a view with NSTableView. Each cell of NSTableView, draws it's content with shadow. Code like this:
NSShadow* shadow = [[NSShadow alloc] init];
[shadow setShadowOffset:NSMakeSize(2.0, -2.0)];
[shadow setShadowBlurRadius:2.0];
[shadow setShadowColor:[[NSColor blackColor] colorWithAlphaComponent:0.3]];
[shadow set];
[self.stringValue drawInRect:NSMakeRect(x, y, w - 60, h) withAttributes:attributes];
When my NSTableView has less than ~50 rows with CALayer turned on OR when i turn off CALayer in IB, i have cells drawn like that: http://i.stack.imgur.com/zMxmK.png
But when my NSTableView has more than ~50 row AND i turn on CALayer in IB, i have cells drawn like that: http://i.stack.imgur.com/IMyIP.png
How can it be? I break my had :(

Try changing -2.0 to +2.0 depending on [[NSGraphicsContext currentContext] isFlipped] property.
Try changing it depending on whether the view is layered and CALayer has isGeometryFlipped on.
If you have flipping transformation(s) applied, note they won't affect NSShadow:
Shadows are always drawn in the default user coordinate space, regardless of any transformations applied to that space. This means that rotations, translations and other transformations of the current transformation matrix (the CTM) do not affect the resulting shadow. Another way to think about this is that changes to the CTM do not move or change the apparent position of the shadow’s light source.

Related

Apply NSAffineTransform to NSView

I want to do a couple of simple transforms on an NSView subclass to flip it on the X axis, the Y axis, or both. I am an experienced iOS developer but I just can't figure out how to do this in macOS. I have created an NSAffineTransform with the required translations and scales, but cannot determine how to actually apply this to the NSView. The only property I can find which will accept any kind of transform is [[NSView layer] transform], but this requires a CATransform3D.
The only success I have had is using the transform to flip the image if an NSImageView, by calling lockFocus on a new, empty NSImage, creating the transform, then drawing the unflipped image inside the locked image. This is far from satisfactory however, as it does not handle any subviews and is presumably more costly than applying the transform directly to the NSView/NSImageView.
This was the solution:
- (void)setXScaleFactor:(CGFloat)xScaleFactor {
_xScaleFactor = xScaleFactor;
[self setNeedsDisplay];
}
- (void)setYScaleFactor:(CGFloat)yScaleFactor {
_yScaleFactor = yScaleFactor;
[self setNeedsDisplay];
}
- (void)drawRect:(NSRect)dirtyRect {
NSAffineTransform *transform = [[NSAffineTransform alloc] init];
[transform scaleXBy:self.xScaleFactor yBy:self.yScaleFactor];
[transform set];
[super drawRect:dirtyRect];
}
Thank you to l'L'l for the hint about using NSGraphicsContext.
I can't believe how many hours of searching and experimenting I had to do before I was able to simply flip an image horizontally in AppKit. I cannot upvote this question and mashers' answer enough.
Here is an updated version of my solution for swift to flip an image horizontally. This method is implemented in an NSImageView subclass.
override func draw(_ dirtyRect: NSRect) {
// NSViews are not backed by CALayer by default in AppKit. Must request a layer
self.wantsLayer = true
if self.flippedHoriz {
// If a horizontal flip is desired, first multiple every X coordinate by -1. This flips the image, but does it around the origin (lower left), not the center
var trans = AffineTransform(scaledByX: -1, byY: 1)
// Add a transform that moves the image by the width so that its lower left is at the origin
trans.append(AffineTransform(translationByX: self.frame.size.width, byY: 0)
// AffineTransform is bridged to NSAffineTransform, but it seems only NSAffineTransform has the set() and concat() methods, so convert it and add the transform to the current graphics context
(trans as NSAffineTransform).concat()
}
// Don't be fooled by the Xcode placehoder. This must be *after* the above code
super.draw(dirtyRect)
}
The behavior of the transforms also took a bit of experimentation to understand. The help for NSAffineTransform.set() explains:
it removes the existing transformation matrix, which is an accumulation of transformation matrices for the screen, window, and any superviews.
This will very likely break something. Since I wanted to still respect all the transformations applied by the window and superviews, the concat() method is more appropriate.
concat() multiplies the existing transform matrix by your custom transform. This is not exactly cumulative, though. Each time draw is called, your transform is applied to the original transform for the view. So repeatedly calling draw doesn't continuously flip the image. Because of this, to not flip the image, simply don't apply the transform.

drawWithFrame: draws NSTextFieldCell background out of registration with frame

I'm failing to have a NSTextFieldCell created and drawn programmatically recreate the same visual appearance equivalently-configured NSTextFieldCells have when built in Interface Builder and drawn automatically by Cocoa.
Context is a custom NSView that displays content that user can select, delete or replace. Though this content is non-textual, I'd like the field to resemble an NSTextField, so I'm trying to press an NSTextFieldCell into service to draw a bezel, background fill, etc., that would match a corresponding NSTextField's.
I setup the cell as follows (during custom control init()):
textFieldCell = [[NSTextFieldCell alloc] init];
[textFieldCell setDrawsBackground: YES];
[textFieldCell setBackgroundColor: NSColor.blueColor];
[textFieldCell setBackgroundStyle: NSBackgroundStyleNormal];
[textFieldCell setBezeled: YES];
[textFieldCell setStringValue: #""];
and then in custom control's drawRect,
- (void) drawRect: (NSRect) rect
{
// Draw an NSTextField-like background
if( isSelectAndDeletable)
{
[textFieldCell drawWithFrame: [self bounds] inView: self];
}
... // more content drawing here
}
This produces on onscreen 1-pixel frame of the appropriate thickness and size, and fills it with blue. But the frame itself is gray, not blue, and the fill touches the frame on top (no white margin) and misses it by two pixels at the bottom (thick white margin). By contrast, when I let Cocoa draw my textFieldCells either as part of an NSTextField or as standalone cells created in Interface Builder, the frame is drawn WITH the cell's background color and the fill is inset with one pixel white margin all around.
This picture demonstrates the problem. It shows three NSTextFieldCells, the second of which is (defectively) drawn by my own call to drawWithFrame:inView:, the other two (correctly) directly by the runtime.
Any ideas what I'm doing wrong? I feel like I am passing the proper rect (the frame is in the correct location), and the single call produces both the correct stroke (frame) and incorrect fill (background). Is this some misconfigured bezel effect? I can hand draw the effect I'm after but would rather stick with NSTextFieldCell if I can fix my approach here.

Draw NSView background partially, with a gradient

I have a NSView, subclassed, with custom background drawing, filling it with a gradient.
In IB, I've put a checkbox on it, somewhere in the middle.
This is the drawRect method.
-(void) drawRect:(NSRect)dirtyRect {
CGFloat sc = 0.9f;
CGFloat ec = 0.6f;
NSColor* startingColor = [NSColor colorWithDeviceRed:sc green:sc blue:sc alpha:1];
NSColor* endingColor = [NSColor colorWithDeviceRed:ec green:ec blue:ec alpha:1];
NSGradient *grad = [[NSGradient alloc] initWithStartingColor:startingColor endingColor:endingColor];
[grad drawInRect:dirtyRect angle:270];
}
What happens is, this same method gets called to draw the whole view area first and then for the part, where NSButton (checkbox) lies on top of it. OF course the checkbox background is drawn with a complete gradient and it is not right, since the portion is much smaller. The same happens with other controls I put on the said NSView.
What is the suggested approach on such thing?
One option is to make controls height the same as the views' but this will result in problems in the future.
The answer is, always draw the WHOLE area of the view, not just the dirtyRect
[grad drawInRect:[self bounds] angle:270];

How do I draw an NSString at an angle?

Is there a set of string attributes I can specify that will draw the text at an angle when I call:
[label drawAtPoint:textStart withAttributes:attributes];
Here's an example that uses a transform to rotate the drawing context. Essentially it's just like setting a color or shadow, just make sure to use -concat instead of -set.
CGFloat rotateDeg = 4.0f;
NSAffineTransform *rotate = [[NSAffineTransform alloc] init];
[rotate rotateByDegrees:rotateDeg];
[rotate concat];
// Lock focus if needed and draw strings, images here.
[rotate release];
NSString itself doesn't have rotation, but you can rotate the context. The string will always be drawn "horizontally" as far as the coordinate space goes, but what actual direction that corresponds to depends on the context. Just use NSAffineTransform to spin it as needed.

How do I get the inner/client size of a NSView subclass?

I am doing manual layouting for my Cocoa application and at some point I need to figure out what the inner size of a NSView subclass is. (E.g. What is the height available for my child view inside of a NSBox?)
One of the reasons is that I am using a coordinate system with origin at the top-left and need to perform coordinate transformations.
I could not figure out a way to get this size so far and would be glad if somebody can give me a hint.
Another very interesting property I would like to know is the minimum size of a view.
-bounds is the one you're looking for in most views. NSBox is a bit of a special case, however, since you want to look at the bounds of the box's content view, not the bounds of the box view itself (the box view includes the title, edges, etc.). Also, the bounds rect is always the real size of the box, while the frame rect can be modified relative to the bounds to apply transformations to the view's contents (such as squashing a 200x200 image into a 200x100 frame).
So, for most views you just use [parentView bounds], and for NSBox you'll use [[theBox contentView] bounds], and you'll use [[theBox contentView] addSubview: myView] rather than [parentView addSubview: myView] to add your content.
Unfortunately, there is no standard way to do this for all NSView subclasses. In your specific example, the position and size of a child view within an NSBox can be computed as follows:
NSRect availableRect = [someNSBox bounds];
NSSize boxMargins = [someBox contentViewMargins];
availableRect = NSInsetRect(availableRect, boxMargins.width, boxMargins.height);
If you find yourself using this often, you could create a category on NSBox as follows:
// MyNSBoxCategories.h
#interface NSBox (MyCategories)
- (NSRect)contentFrame;
#end
// MyNSBoxCategories.m
#implementation NSBox (MyCategories)
- (NSRect)contentFrame
{
NSRect frameRect = [self bounds];
NSSize margins = [self contentViewMargins];
return NSInsetRect(frameRect, margins.width, margins.height);
}
#end
And you would use it like so:
#import "MyNSBoxCategories.h"
//...
NSRect frameRect = [someNSBox contentFrame];
[myContentView setFrame:frameRect];
[someNSBox addSubview:myContentView];
The bounds property of NSView returns an NSRect with the origin (usually (0,0)) and the size of an NSView. See this Apple Developer documentation page.
I'm not sure (I never had to go too deep in that stuff), but isn't it [NSView bounds]?
http://www.cocoadev.com/index.pl?DifferenceBetweenFrameAndBounds

Resources