How to draw over a subview of NSView - objective-c

I'm trying to draw at the top of my NSView which has some subviews.
In fact I'm trying to reproduce the connection line style of Interface Builder. Here is the code I'm using for the moment:
- (void)drawRect:(CGRect)dirtyRect
{
// Background color
[[NSColor whiteColor] setFill];
NSRectFill(dirtyRect);
// Draw line
if(_connecting)
{
CGContextRef c = [[NSGraphicsContext currentContext] graphicsPort];
[[NSColor redColor] setStroke];
CGContextMoveToPoint(c, _start.x, _start.y);
CGContextAddLineToPoint(c, _end.x, _end.y);
CGContextSetLineWidth(c, LINE_WIDTH);
CGContextClosePath(c);
CGContextStrokePath(c);
}
}
The first part is to color my NSView (if you know an other way, tell me please 'cause I come from iPhone development and I miss the backgroundColor property of UIView)
Then if a connection if detected, I draw it with 2 NSPoints. This code works but I didn't get it to draw over subviews, only on the first NSView.

A parent view cannot draw over its subviews. You would have to place another view over the subviews and draw the line there.

Related

Why does adding a top border to a subview does not resize with auto layout/device rotation?

(using Xcode 6 and iOS 8)
How can I create a top border in a sub UIView that sizes correctly when the view changes due to auto layout or device rotation?
I have code draws a line as the top border but it uses the size of what I have in Interface Builder. It does not size correctly after auto layout resizes the view nor after device rotation.
In the UIViewController, I tried [self.view layoutSubviews], [self.view layoutIfNeeded], [self.myCustomView layoutIfNeeded], and so on, in following method because when this method is called, the view has been resized by auto layout. But doesn't change the size of the line.
UIViewController:
- (void) viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[self.myCustomView layoutIfNeeded];
}
Hierarchy is:
UIViewController -> ViewController's View -> my custom UIView
Tried 2 different methods to draw the border which do draw the lines in my-custom-uiview. Neither one is recalled after the view is resized by auto layout or device rotation. This really shouldn't be that hard!
myCustomUIView:
First method:
// in initWithCoder, initWithFrame, awakeFromNib methods
CALayer *border = [CALayer layer];
border.frame = CGRectMake(self.bounds.origin.x, self.bounds.origin.y, self.bounds.size.width, 2.0f);
border.backgroundColor = [UIColor blackColor].CGColor;
[self.contentView.layer addSublayer:border];
Second method:
- (void) drawRect:(CGRect)rect {
UIBezierPath *line = [UIBezierPath bezierPath];
[line moveToPoint:CGPointMake(0, 0)];
[line addLineToPoint:CGPointMake(self.bounds.size.width, 0)];
[line setLineWidth:2.0f];
[[UIColor blackColor] setStroke];
[line stroke];
}

How to draw background image repeatedly with Mac OS

I am new in Mac OS programming. I would like to draw image repeatedly in background since my original image is small. Here is the code of drawing the image, but it seems enlarge the image which means it only draw one image instead of multiple.
// overwrite drawRect method of NSView
-(void)drawRect:(NSRect)dirtyRect{
[[NSImage imageNamed:#"imageName.png"] drawInRect:dirtyRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1];
[super drawRect:dirtyRect];
}
This should work for you...
// overwrite drawRect method of NSView
-(void)drawRect:(NSRect)dirtyRect{
[super drawRect:dirtyRect];
// Develop color using image pattern
NSColor *backgroundColor = [NSColor colorWithPatternImage:[NSImage imageNamed:#"imageName.png"]];
// Get the current context and save the graphic state to restore it once done.
NSGraphicsContext* theContext = [NSGraphicsContext currentContext];
[theContext saveGraphicsState];
// To ensure that pattern image doesn't truncate from top of the view.
[[NSGraphicsContext currentContext] setPatternPhase:NSMakePoint(0,self.bounds.size.height)];
// Set the color in context and fill it.
[backgroundColor set];
NSRectFill(self.bounds);
[theContext restoreGraphicsState];
}
Note: You may like to consider creating backgroundColor as part of the object for optimization as drawRect is called pretty often.

Proper place to put NSTextField drawing code?

I am working on creating some custom Cocoa components. Currently I'm trying to figure out how to draw custom NSTextFields.
I have overridden the drawRect method on my subclass but when i start typing, i get a double rectangle like this http://imgur.com/a/LpUMy.
Here is my drawRect method
- (void)drawRect:(NSRect)dirtyRect
{
[NSGraphicsContext saveGraphicsState];
[[NSBezierPath bezierPathWithRoundedRect:dirtyRect xRadius:5.0f yRadius:5.0f] setClip];
[[NSColor grayColor] setFill];
NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver);
[NSGraphicsContext restoreGraphicsState];
[NSGraphicsContext saveGraphicsState];
NSRect rect = NSInsetRect(dirtyRect, 1.0f, 1.0f);
[[NSBezierPath bezierPathWithRoundedRect:rect xRadius:5.0f yRadius:5.0f] setClip];
[[NSColor whiteColor] setFill];
NSRectFillUsingOperation(rect, NSCompositeSourceOver);
[NSGraphicsContext restoreGraphicsState];
}
UPDATE:
I moved my drawing code into a NSTextFieldCell subclass as so
- (void)drawWithFrame:(NSRect)frame inView:(NSView *)controlView {
[NSGraphicsContext saveGraphicsState];
[[NSBezierPath bezierPathWithRoundedRect:frame xRadius:5.0f yRadius:5.0f] setClip];
[[NSColor grayColor] setFill];
NSRectFillUsingOperation(frame, NSCompositeSourceOver);
[NSGraphicsContext restoreGraphicsState];
[NSGraphicsContext saveGraphicsState];
[[NSBezierPath bezierPathWithRoundedRect:NSInsetRect(frame, 1.0f, 1.0f) xRadius:3.0f yRadius:3.0f] setClip];
[[NSColor whiteColor] setFill];
NSRectFillUsingOperation(frame, NSCompositeSourceOver);
[NSGraphicsContext restoreGraphicsState];
}
But as soon as you are done editing it draws over the text, even though the cursor is still there? Any suggestions? I've tried drawing the title but it still happens.
Thanks for your help.
Answer:
NSCell Custom Highlight
By calling super drawInteriorWithFrame:inView I was able to stop the weird text disappearing issues.
It looks to me like you've ended up drawing inside the drawing of your superclass's (NSTextField's) drawRect: implementation. You haven't called super but it still manages to draw itself. I'm not sure why myself, but some NSControls such as text fields and buttons, when subclassed, will draw themselves regardless of whether or not you call drawRect: on them. For example, if you subclass a plain NSButton, implement drawRect: and don't call super, it'll draw the button anyways. Potentially over whatever you drew, which has caused confusion in the past. The easiest solution is to not subclass NSTextField, and see if there's another class you can subclass (like NSTextFieldCell mentioned in the comment).

NSView drawRect interfering with subviews?

I have an nsview and i use draw rect to draw an image for background. It also has 3 subviews nsbuttons. The problem is, whenever the mouse is down on a button, the other buttons disappear. But when I remove the draw rect method, this doesn't happen. So I am guessing this has to do with the draw rect method for drawing images.
How can I avoid this?
Thanks.
EDIT:
Ok, i figured out where the problem is. Basically, I have an NSMenuItem, and I am putting a view inside it with 3 buttons. But in NSMenu, at the top, there's a padding of 4 pixels. So, basically, to remove that padding I used the solution provided here:
Gap above NSMenuItem custom view
From the solution there's a line in the drawRect method:
[[NSBezierPath bezierPathWithRect:fullBounds] setClip];
The moment, i remove this line, and the button behave properly. But then, the padding on top doesn't go away.
Here's my drawRect:
- (void) drawRect:(NSRect)dirtyRect {
[[NSGraphicsContext currentContext] saveGraphicsState];
NSRect fullBounds = [self bounds];
fullBounds.size.height += 4;
[[NSBezierPath bezierPathWithRect:fullBounds] setClip];
NSImage *background = [NSImage imageNamed:#"bg.png"];
[background drawInRect:fullBounds fromRect:NSZeroRect operation:NSCompositeCopy fraction:100.0];
[[NSGraphicsContext currentContext] restoreGraphicsState];
}
The solution to the linked question doesn't include saving and restoring the graphics state, which is a good idea when you are modifying one that you didn't create. Give this a try:
- (void)drawRect:(NSRect)dirtyRect {
// Save the current clip rect that has been set up for you
[NSGraphicsContext saveGraphicsState];
// Calculate your fullBounds rect
// ...
// Set the clip rect
// ...
// Do your drawing
// ...
// Restore the correct clip rect
[NSGraphicsContext restoreGraphicsState]
Are you sure that those buttons are actually subviews, and not just placed over the view you're drawing?

Subclassing NSScrollView drawRect: Method

I'm customizing the UI for one of my apps, and the idea is that a text area is initially bordered gray when out of focus, and when it comes into focus, the border becomes bright white. My app uses a dark theme, and for a single-lined NSTextField, this works great.
I'm running into problems with a subclassed NSTextView, however. In order to alter the border properly, I ended up having to actually subclass the parent NSScrollView, but am still seeing strange behavior. (See screenshot below.) I want the red box to fill the entire scroll view, as this would allow me to stroke (instead of filling, which is just for testing) the path, producing a nice border. Instead, the red box seems to be only filling to the internal child view.
The following code snippet, which is for the NSScrollView subclass:
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
NSRect borderRect = self.bounds;
borderRect.origin.y += 1;
borderRect.size.width -= 1;
borderRect.size.height -= 4;
BOOL inFocus = ([[self window] firstResponder] == self);
if (!inFocus) {
inFocus = [self anySubviewHasFocus:self];
}
if (inFocus) {
[[NSColor colorWithDeviceRed:.8 green:.2 blue:0 alpha:1] set];
} else {
[[NSColor colorWithDeviceRed:.1 green:.8 blue:0 alpha:1] set];
}
[NSGraphicsContext saveGraphicsState];
[[NSGraphicsContext currentContext] setShouldAntialias:NO];
[NSBezierPath fillRect:borderRect];
[NSGraphicsContext restoreGraphicsState];
NSLog(#"My bounds: %#", NSStringFromRect(borderRect));
NSLog(#"Super (%#) bounds: %#", [self superview], NSStringFromRect(borderRect));
}
Produces the screenshot as seen below. Also, see the output in the log, which suggests that the entire view should be filled. This is the only output that is ever shown, regardless of the size of the text inside. Entering carriage returns increases the height of the red box, but does not produce different output. (And I would like the red box to fill the entire bounds.)
2011-04-08 21:30:29.789 MyApp[6515:903] My bounds: {{0, 1}, {196, 87}}
2011-04-08 21:30:29.789 MyApp[6515:903] Super (<EditTaskView: 0x3a0b150>) bounds: {{0, 1}, {196, 87}}
Edit: Thanks to Josh Caswell for his answer. See below for the proper behavior when not focused, and when focused.
As ughoavgfhw noted, NSScrollView doesn't usually do any drawing, and probably has a weird interaction with its child views in that way. I'd suggest putting something like the following in your text view's drawing code to draw this custom focus ring that you want*:
// We're going to be modifying the state for this,
// so allow it to be restored later
[NSGraphicsContext saveGraphicsState];
// Choose the correct color; isFirstResponder is a custom
// ivar set in becomeFirstResponder and resignFirstResponder
if( isFirstResponder && [[self window] isKeyWindow]){
[myFocusedColor set];
}
else {
[myNotFocusedColor set];
}
// Create two rects, one slightly outset from the bounds,
// one slightly inset
NSRect bounds = [self bounds];
NSRect innerRect = NSInsetRect(bounds, 2, 2);
NSRect outerRect = NSMakeRect(bounds.origin.x - 2,
bounds.origin.y - 2,
bounds.size.width + 4,
bounds.size.height + 4);
// Create a bezier path using those two rects; this will
// become the clipping path of the context
NSBezierPath * clipPath = [NSBezierPath bezierPathWithRect:outerRect];
[clipPath appendBezierPath:[NSBezierPath bezierPathWithRect:innerRect]];
// Change the current clipping path of the context to
// the enclosed area of clipPath; "enclosed" defined by
// winding rule. Drawing will be restricted to this area.
// N.B. that the winding rule makes the order that the
// rects were added to the path important.
[clipPath setWindingRule:NSEvenOddWindingRule];
[clipPath setClip];
// Fill the rect; drawing is clipped and the inner rect
// is not drawn in
[[NSBezierPath bezierPathWithRect:outerRect] fill];
[NSGraphicsContext restoreGraphicsState];
This should be a reasonable approximation of what AppKit does when it draws a focus ring. Of course, AppKit is sort of allowed to draw outside a view's bounds -- I can't guarantee that this is completely safe, but you seem to get a margin of 3 px to play with. You could draw the ring entirely inside the bounds if you wanted. A true focus ring extends slightly (2 px) inside the view anyways (as I've done here).
Apple docs on Setting the Clipping Region.
EDIT: After re-reading your comments on the question, I realize I may have long-winded-ly buried the real answer. Try either subclassing NSClipView and switching your scroll view's clip view for that, or using a custom view for the document view.
*: You could also put this in the drawing code of a custom view subclass which is set as the document view of the NSScrollView; then your text view could be a subview of that. Or substitute a custom NSClipView subclass.