Drawing issues after subclassing NSCollectionView - objective-c

OK, here's what I have done:
I have an NSCollectionView
I wanted to be able to enable "selecting" items, and drawing a custom border when an items is selected
I subclassed NSCollectionViewItem (to enable selection)
I subclassed NSView for the NSCollectionViewItem view, in order to draw the border
The code
The view item
#implementation MSLibraryCollectionViewItem
- (void)setSelected:(BOOL)flag
{
[super setSelected:flag];
[(MSLibraryCollectionViewView*)[self view] setSelected:flag];
[(MSLibraryCollectionViewView*)[self view] setNeedsDisplay:YES];
}
The custom view
#implementation MSLibraryCollectionViewView
/***************************************
Initialisation
***************************************/
- (MSLibraryCollectionViewView*)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
}
return self;
}
/***************************************
Drawing
***************************************/
- (void)drawRect:(NSRect)rect
{
if ([self selected]) {
//[[NSColor redColor] setFill];
//NSRectFill(rect);
//[super drawRect:rect];
NSColor* gS = [NSColor colorWithCalibratedRed:0.06 green:0.45 blue:0.86 alpha:1.0];
NSColor* gE = [NSColor colorWithCalibratedRed:0.12 green:0.64 blue:0.94 alpha:1.0];
NSGradient* g = [[NSGradient alloc] initWithStartingColor:gE endingColor:gS];
NSColor *borderColor = [NSColor colorFromGradient:g];
NSRect frameRect = [self bounds];
if(rect.size.height < frameRect.size.height)
return;
NSRect newRect = NSMakeRect(rect.origin.x+5, rect.origin.y+5, rect.size.width-10, rect.size.height-10);
NSBezierPath *textViewSurround = [NSBezierPath bezierPathWithRoundedRect:newRect xRadius:7 yRadius:7];
[textViewSurround setLineWidth:2.0];
[borderColor set];
[textViewSurround stroke];
}
}
However, the seems to be something wrong with drawing. For example:
When resizing the Collection View's container, a weird line appears at the outer box
When an Collection View item is not 100% visible (e.g. because it's been scrolled down), the selection border doesn't appear at all (while I would expect it to draw just the visible portion).
Some Examples
What's going on?
P.S. I'm not a guru with drawing and custom views in Cocoa - so any ideas/help is more than welcome!

You switched from asking about a collection view to talking about an outline view, but I assume that was just a mental hiccup.
When an Outline View item is not 100% visible (e.g. because it's been scrolled down), the selection border doesn't appear at all
(while I would expect it to draw just the visible portion).
That's because of this code in your -drawRect:.
if(rect.size.height < frameRect.size.height)
return;
It's specifically avoiding drawing a partial selection outline.
Regarding the weird line, I doubt that has to do with your collection item view's custom drawing. Does it stop happening if you disable the custom drawing? You could experiment with using an ordinary color rather than using the third-party +colorFromGradient: code you're using.
By the way, this line:
NSRect newRect = NSMakeRect(rect.origin.x+5, rect.origin.y+5, rect.size.width-10, rect.size.height-10);
could be written more simply as:
NSRect newRect = NSInsetRect(rect, 5, 5);

Related

Objective C - Unwanted black background behind circle

I'm trying to draw a green circle using drawRect but I'm getting a black background in the rectangle area around the green circle.
I need bounding rectangle's fill to be transparent.
Here my DrawCircle class:
- (void)drawRect:(CGRect)rect {
UIBezierPath *ballBezierPath = [UIBezierPath rect];
[[UIColor greenColor] setFill];
[ballBezierPath stroke];
self.opaque = NO;
self.backgroundColor = [UIColor clearColor];
}
My research indicated that adding the last two lines for setting opaque and backgroundColor would fix the issue, but it still doesn't work.
Here's where I'm adding the sub view in my ViewController's viewDidLoad method:
CGRect positionFrame = CGRectMake(160,160,200,200);
DrawCircle *drawBallView = [[DrawCircle alloc] initWithFrame:positionFrame];
[view1 addSubview:drawBallView];
Have I missed something or is there a different approach I can take?
There are a few problems with your current approach:
the properties are set too late, after you are actually drawing
you are setting those properties on each draw now, they only need to be set once
changing properties could trigger a redraw, and you'll end up redrawing over and over
you are overwriting drawRect: without a call to super. To make the backgroundColor work to either fill the rectangle yourself with that color, or have the superclass do it.
Move your view configuration to the initializers, drawRect is not a good to do any of that. Implement initWithFrame and initWithCoder (to support Interface Builder.
- (id) initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setupView];
}
return self;
}
- (id) initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self setupView];
}
return self;
}
- (void) setupView {
self.opaque = NO;
self.backgroundColor = [UIColor clearColor];
}

UIButton displaying a triangle

I have a UIButton and i want it to display a triangle. Is there a function to make it a triangle? Since im not using a UIView class im not sure how to make my frame a triangle.
ViewController(m):
- (IBAction)makeTriangle:(id)sender {
UIView *triangle=[[UIView alloc] init];
triangle.frame= CGRectMake(100, 100, 100, 100);
triangle.backgroundColor = [UIColor yellowColor];
[self.view addSubview: triangle];
Do i have to change my layer or add points and connect them to make a triangle with CGRect?
If im being unclear or not specific add a comment. Thank you!
A button is a subclass of UIView, so you can make it any shape you want using a CAShape layer. For the code below, I added a 100 x 100 point button in the storyboard, and changed its class to RDButton.
#interface RDButton ()
#property (strong,nonatomic) UIBezierPath *shape;
#end
#implementation RDButton
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
self.titleEdgeInsets = UIEdgeInsetsMake(30, 0, 0, 0); // move the title down to make it look more centered
self.shape = [UIBezierPath new];
[self.shape moveToPoint:CGPointMake(0,100)];
[self.shape addLineToPoint:CGPointMake(100,100)];
[self.shape addLineToPoint:CGPointMake(50,0)];
[self.shape closePath];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = self.shape.CGPath;
shapeLayer.fillColor = [UIColor yellowColor].CGColor;
shapeLayer.strokeColor = [UIColor blueColor].CGColor;
shapeLayer.lineWidth = 2;
[self.layer addSublayer:shapeLayer];
}
return self;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if ([self.shape containsPoint:[touches.anyObject locationInView:self]])
[super touchesBegan:touches withEvent:event];
}
The touchesBegan:withEvent: override restricts the action of the button to touches within the triangle.
A view's frame is always a rect, which is a rectangle. Even if you apply a transform to it so it no longer looks like a rectangle, the view.frame property will still be a rectangle -- just the smallest possible rectangle that contains the new shape you have produced.
So if you want your UIButton to look like a triangle, the simplest solution is probably to set its type to UIButtonTypeCustom and then to set its image to be a png which shows a triangle and is transparent outside of the triangle.
Then the UIButton itself will actually be rectangle, but will look like a triangle.
If you want to get fancy, you can also customize touch delivery so that touches on the transparent part of the PNG are not recognized (as I believe they would be by default), but that might be a bit trickier.

NSView with fill ( pattern image) scrolls when window changes size

I have an NSView with a drawRect
- (void)drawRect:(CGRect)rect {
// Drawing code
NSPoint origin = [self visibleRect].origin;
[[NSGraphicsContext currentContext]
setPatternPhase:NSMakePoint(origin.x, origin.y)];
[[NSColor colorWithPatternImage: self.image] set];
[NSBezierPath fillRect: [self bounds]];
}
It draws my pattern perfectly, but i can see the pattern scroll when i change the the size of my window.
i have tried to set the view isFlipped to YES but that doesn't change anything.
You need to do some off-screen drawing first and then draw that result onto the view. For example you can use a blank NSImage of the exact same size as the view, draw the pattern on that image and then draw that image on the view.
Your code may look something like that:
- (void)drawRect:(NSRect)dirtyRect
{
// call super
[super drawRect:dirtyRect];
// create blank image and lock drawing on it
NSImage* bigImage = [[[NSImage alloc] initWithSize:self.bounds.size] autorelease];
[bigImage lockFocus];
// draw your image patter on the new blank image
NSColor* backgroundColor = [NSColor colorWithPatternImage:bgImage];
[backgroundColor set];
NSRectFill(self.bounds);
[bigImage unlockFocus];
// draw your new image
[bigImage drawInRect:self.bounds
fromRect:NSZeroRect
operation:NSCompositeSourceOver
fraction:1.0f];
}
// I think you may also need to flip your view
- (BOOL)isFlipped
{
return YES;
}
Swift
A lot has changed, now things are easier, unfortunately part of objective-C's patrimony is lost and when it comes to Cocoa, Swift is like an orphan child. Anyways, based on Neovibrant's we can deduct the solution.
Subclass NSView
Override draw method
Call parent method (this is important)
Set a fill on buffer within the bounds of the view
Draw fill on buffer
code
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let bgimage : NSImage = /* Set the image you want */
let background = NSColor.init(patternImage: bgimage)
background.setFill()
bgimage.draw(in: self.bounds, from: NSZeroRect, operation: .sourceOver, fraction: 1.0)
}

Highlighting an Edited NSBrowserCell & Draw A Focus Ring on its NSText?

What I'm trying to do
I'm trying to add edit-in-place functionality to the Connection Kit's NSBrowser. I'd like this behaviour to be functionally and visually similar to Finder's implementation.
The visual effect I'm aiming for
What I've got so far
The arrows indicate focus ring & cell highlighting in Finder's implementation, and the lack of it in mine.
I have tried
Setting the background colour of the cell in the controller, in it's drawInteriorWithFrame method
The same for the field editor
setFocusRingType:NSFocusRingTypeDefault for the field editor & cell both in the controller & the draw method
Manually drawing the highlight color in the draw method
Various combinations of the above, and undoubtedly some I've forgotten.
The best I've managed was getting the area surrounding the cell's image coloured with the highlight colour.
Is there some fundamental that I'm missing here? Could someone please suggest a starting point for approaching this? Is drawInteriorWithFrame the place to be doing this?
I've got editing working fine - I'm just having trouble with the visual aspects.
Code to allow editing:
// In the main controller
int selectedColumn = [browser selectedColumn];
int selectedRow = [browser selectedRowInColumn:selectedColumn];
NSMatrix *theMatrix = [browser matrixInColumn:selectedColumn];
NSRect cellFrame = [theMatrix cellFrameAtRow:selectedRow column:0];
NSText *fieldEditor = [[browser window] fieldEditor:YES
forObject:editingCell];
[cell editWithFrame:cellFrame
inView:theMatrix
editor:fieldEditor
delegate:self
event:nil];
And in my subclass of NSBrowserCell:
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
image = [[self representedObject] iconWithSize:[self imageSize]];
[self setImage:image];
NSRect imageFrame, highlightRect, textFrame;
// Divide the cell into 2 parts, the image part (on the left) and the text part.
NSDivideRect(cellFrame, &imageFrame, &textFrame, ICON_INSET_HORIZ + ICON_TEXT_SPACING + [self imageSize].width, NSMinXEdge);
imageFrame.origin.x += ICON_INSET_HORIZ;
imageFrame.size = [self imageSize];
[super drawInteriorWithFrame:cellFrame inView:controlView];
}
- (void)editWithFrame:(NSRect)aRect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)anObject event:(NSEvent *)theEvent {
NSRect imageRect, textRect;
NSDivideRect(aRect , &imageRect, &textRect, 20, NSMinXEdge);
self.editing = YES;
[super editWithFrame: textRect inView: controlView editor:textObj delegate:anObject event:theEvent];
}
You have to draw the focus ring yourself.
Add the following in drawWithFrame in your NSBrowserCell subclass
[NSGraphicsContext saveGraphicsState];
[[controlView superview] lockFocus];
NSSetFocusRingStyle(NSFocusRingAbove);
[[NSBezierPath bezierPathWithRect:NSInsetRect(frame,-1,-1)] fill];
[[controlView superview] unlockFocus];
[NSGraphicsContext restoreGraphicsState];
Try subclassing field editor object and override drawRect function like this :
- (void)drawRect:(NSRect)rect {
[super drawRect:rect];
NSSetFocusRingStyle(NSFocusRingOnly);
NSRectFill([self bounds]);
}
Hope this helps.

CALayer - clear internal rendering context in favor of custom drawing code

When subclassing an CALayer and implementing the drawInContext method, I would assume that any drawing I do within there is all that will show up, but instead if I set (for example) borderWidth/borderColor then CALayer will draw a border on it's own above all my custom drawing code.
This is a CALayer subclass:
#implementation MyCustomCALayer
- (id)init
{
self = [super init];
if(self)
{
[self setNeedsDisplayOnBoundsChange:YES];
}
return self;
}
- (void)drawInContext:(CGContextRef)context
{
CGRect rect = CGContextGetClipBoundingBox(context);
CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
CGContextFillRect(context, rect);
}
#end
Created in a UIView something like:
- (void)ensureLayer
{
if(myLayer)
return;
myLayer = [[[MyCustomCALayer alloc] init] autorelease];
myLayer.borderColor = [UIColor greenColor].CGColor;
myLayer.borderWidth = 1;
myLayer.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
}
- (void)layoutSublayersOfLayer:(CALayer *)layer
{
[super layoutSublayersOfLayer:layer];
[self ensureLayer];
if(![[layer.sublayers objectAtIndex:0] isEqual:myLayer])
[layer insertSublayer:myLayer atIndex:0];
}
What happens, is the MyCustomCALayer fills a rectangle with red, this is what I would expect to see and nothing else, since i've implemented the drawInContext method, but instead I see a red rectangle with a green border on top, always on top, i've tried just about every combination I can think of to get rid of the green border being drawn and cannot figure it out.
My reasoning is I would like to use the borderWidth and borderColor and other properties of the CALayer instead of creating my own properties, because the code that I need to draw contains a border, a fill, etc... but the rendering I need to do is not a simple shape. So the only way i've found around this is to set the borderWidth to 0 and add my own property to my subclass, like myBorderWidth, which is ridiculous.
This is done with the latest iOS SDK, but i'd imagine it's the same for Mac.
Hope this makes sense, any thoughts?
Thanks!
You’re out of luck; CoreAnimation doesn’t support overriding its implementation of rendering for the basic layer properties. Please do file a bug.