Drawing focus ring in NSTextView when using scaleUnitSquareToSize: - objective-c

As of OSX 10.7, Apple suggests using drawFocusRingMask and focusRingMaskBounds methods as the way to draw a focus ring around NSView based objects instead of how it was done previously in 10.6 (using NSSetFocusRingStyle() in drawRect:). (see release notes).
Tested on OSX 10.10 :
I have an NSTextView descendent which implements basic focus ring drawing as suggested:
- (void)drawFocusRingMask {
NSRectFill([self bounds]);
}
- (NSRect)focusRingMaskBounds {
return [self bounds];
}
This NSTextView is programmatically added directly to the content view (without any scroll views).
This works well for the most part, however, when using scaleUnitSquareToSize: to scale the NSTextView, the focus ring drawn is completely incorrect.
So calling:
[textView scaleUnitSquareToSize:NSMakeSize(1.5, 1.5)];
Looks like this:
Scaling it further up or down results in even more skew of the focus ring rect. Reseting the scale of the NSTextView back to {1.0, 1.0} causes the focus ring to draw correctly again.
I assume this is some sort of a bug in the focus ring code. It seems to preform transformations on the graphics context used for drawing the focus ring before calling drawFocusRingMask / focusRingMaskBounds (internal class _NSAutomaticFocusRing is responsible for this).
Any idea how to fix this? Should I find a way to transform the context to the proper coordinates (so far without success)? Or is there any other way to force the focus ring to behave properly?

Overriding the drawFocusRingMask method (via a sub-class of NSTextField) allows the width of the focus ring to be corrected by "manually" applying the super-view's scale factor, but changing the height doesn't work. My guess is that the height of the focus ring is being pinned to the text size or original text field bounds before scaling.
This all on system 10.7.5. So I'm simply turning the focus ring off when the text field's super-view is in a zoomed state, until I can figure out a workaround or whatever the right thing to do is.

I have struggled with this bug for a long time. Here's a solution that seems to work:
Sub-class the NSTextField or NSTextView so that you are instantiating, e.g., a ZoomableTextField. Override its drawFocusRingMask: method, so that the override does nothing:
- (void)drawFocusRingMask
{
}
Then in the zoomable superview in which a ZoomableTextField is embedded, inside the superview's drawRect: method, call a new method of the ZoomableTextField, drawPossibleFocus, which looks like this:
- (void)drawPossibleFocus
{
NSWindow *win = [self window];
if ([self currentEditor]==[win firstResponder] && [win isKeyWindow]) {
[NSGraphicsContext saveGraphicsState];
NSSetFocusRingStyle(NSFocusRingOnly);
NSRectFill([self frame]);
[NSGraphicsContext restoreGraphicsState];
}
}

Related

Why NSView leaves an image on superview on where it was when I move it?

I am working on a small application on Mac that I need to create customed cursor and move it. I used NSImageView to implement it. However when I call setFrameOrigin (the same to setFrame) it will leaves images on the previous place.
Here is my code:
#property (nonatomic, strong) NSImageView *eraserView;
this is the define
_eraserView = [[NSImageView alloc] initWithFrame:CGRectMake(100, 100, 32, 32)];
_eraserView.image = [NSImage imageNamed:#"EraserCursor"];
[self.view addSubview:_eraserView];
[_eraserView setHidden:YES];
here is the initialization. Everything goes well until now but:
- (void)setImageatPoint:(NSPoint)point
{
[_eraserView setFrameOrigin:point];
}
- (void)hidePenImage
{
[_eraserView setHidden:YES];
}
- (void)unhidePenImage: (BOOL)isEraser
{
[_eraserView setHidden:NO];
}
These are methods I use to change the state of the NSImageView. They will be called by another class using delegate when corresponding events of trackpad occurs.
However every time I change the state of the NSImageView, it seems like it is drawn on the superview.
I debugged it and found there was no extra subviews. And when I use setHidden it has no effect on those tracks. I think it somehow did something to the CALayer, but I have no idea how to fix it.
Screenshots would help but in general if you move a view or change the area of the view that is drawn, you need to redraw.
To do this it kind of depends on how your drawing happens. Calling setNeedsDisplay may not be enough if your implementation of drawRect only draws a sub rect of the view bounds. Cocoa only draws what it is told to draw.
You can erase sections of the view that should be empty by drawing (filling) where it should be empty. That means drawing a color ( NSColor clearColor if nothing else) in the area that was previously drawn.

How to customize a NSSlider

I'm trying to implement a custom slider in Cocoa with 5 values. See my demo project, which can be downloaded here: http://s000.tinyupload.com/index.php?file_id=07311576247413689572.
I've subclassed the NSSliderCell and implemented methods like drawKnob:(NSRect)knobRect and drawBarInside:(NSRect)cellFrame flipped:(BOOL)flipped etc.
I'm facing some issues:
I'm not able to position the knob correctly regarding to the background image. I know that I'm able to change the knob's frame, and I've tried doing some calculation to position the knob correctly, but I'm not able to make it work for my custom slider. Could someone please help me with this?
The height of my custom slider background is 41px. In the drawBarInside:(NSRect)cellFrame flipped:(BOOL)flipped I change the height of the frame to 41px as well, but the entire background is not visible. Why?
I've noticed that the included images (the background and knob) are flipped vertically. Why? Note that the border top is darker in the background compared to the bottom, but this is reversed when I draw the background.
I found a mistake in your calculation of the x position of the knob rectangle: You used the height of the image where you should have used the width.
The cell drawing is being clipped to the frame of the control. Maybe you could expand the control frame when your cell awakes.
You need to use the NSImage method drawInRect:fromRect:operation:fraction:respectFlipped:hints:, and pass YES for the respectFlipped: parameter. Apple's controls generally do use flipped coordinates.
Added: Expanding the frame in awakeFromNib doesn't seem to work, the frame gets set back. Here's something that does work. Instead of overriding drawBarInside:flipped:, add this override:
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
NSRect controlFrame = [controlView frame];
float bgHeight = self.backgroundImage.size.height;
if (controlFrame.size.height < bgHeight)
{
controlFrame.size.height = bgHeight;
[controlView setFrame: controlFrame];
}
[self.backgroundImage
drawInRect: [controlView bounds]
fromRect: NSZeroRect
operation: NSCompositeSourceOver
fraction: 1.0
respectFlipped: YES
hints: NULL];
[self drawKnob];
}

Drawing Transparent Images

I created a custom view to a button, as I need to implement some highlighting when the mouse is over. The class is very simple, and I already implemented mouseEntered: as well as mouseExited:. The view was registered for tracking in the init method (not sure if it's the best place).
The problem is drawing. I keep an ivar mouseOver, set to YES on mouse enter and NO on mouse exited. The other ivar is for the image, called image. The difference between mouse over or not when it comes to drawing, is the transparency. Here is my drawRect::
- (void)drawRect:(NSRect)dirtyRect
{
[image drawAtPoint:NSMakePoint(0.0,0.0)
fromRect:dirtyRect
operation:NSCompositeCopy
fraction:((mouseOver) ? 1.0 : 0.0)];
}
It works nicely, but only when the mouse first entered, apparently. I guess the problem is that the view is not cleared before drawing the other image. I tried adding:
[[NSColor clearColor] set];
NSRectFillUsingOperation(dirtyRect, NSCompositeClear);
But without success. How can I fix this?
[NSColor clearColor] is a purely transparent color. You probably want to fill using a color with some opacity, like, say, [NSColor whiteColor].

Background image for a window in Cocoa framework

I am looking for a perfect solution to set a background image for a window in a cocoa application. I haven't found a solution to this, I am new in objective c, so please anyone help me...
A window in Cocoa has a root-level view called the "content view". This is the view that contains all the others in a window. By default, it's just a plain, blank NSView. But you could easily create your own custom NSView subclass, override the drawRect: method to draw your background image, and use that for your custom view.
However, it might just be easier to use a plain old NSImageView. The advantage of this is that you can set, for example, autosizing behavior to keep the image pinned to one corner (try this with Installer.app by resizing the installer window). You would also be able to make it semi-opaque so that the background shows through a bit. (Again, I'm thinking of Installer.app; your app could be totally different)
Hope that gets you going in the right direction!
Michael Vannorsdel suggests sublassing NSView for the purpose, and I quote:
You'd really be better off making an
NSView subclass and having it draw
the image you want in drawRect:.
- (void)awakeFromNib
{
myImage = [[NSImage alloc] init....
[self setNeedsDisplay:YES];
}
- (void)drawRect:(NSRect)rect
{
NSSize isize = [myImage size];
[myImage drawInRect:[self bounds] fromRect:NSMakeRect(0.0, 0.0,
isize.width, isize.height) operation: NSCompositeCopy fraction:1.0];
}
Read that whole thread on cocoabuilder, it's quite instructive.

UIView subclass draws background despite completely empty drawRect: - why?

So, I have a custom UIView subclass which enables drawing of rounded edges. The thing draws perfectly, however the background always fills the whole bounds, despite clipping to a path first. The border also draws above the rectangular background, despite the fact that I draw the border in drawRect: before the background. So I removed the whole content of drawRect:, which is now virtually empty - nevertheless the background gets drawn!
Anybody an explanation for this? I set the backgroundColor in Interface Builder. Thanks!
Sorry for this monologue. :)
The thing is that the UIView's layer apparently draws the background, which is independent from drawRect:. This is why you can't get rid of the background by overriding drawRect:.
You can either override - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx and make the layer draw whatever you want, or you override - (void) setBackgroundColor:(UIColor *)newColor and don't assign newColor to backgroundColor, but to your own ivar, like myBackgroundColor. You can then use myBackgroundColor in drawRect; to draw the background however you like.
Overriding setBackgroundColor:
Define an instance variable to hold your background color, e.g. myBackgroundColor. In your init methods, set the real background color to be the clearColor:
- (id) initWithCoder:(NSCoder *)aDecoder
{
if ((self = [super init...])) {
[super setBackgroundColor:[UIColor clearColor]];
}
return self;
}
Override:
- (void) setBackgroundColor:(UIColor *)newColor
{
if (newColor != myBackgroundColor) {
[myBackgroundColor release];
myBackgroundColor = [newColor retain];
}
}
Then use myBackgroundColor in your drawRect: method. This way you can use the color assigned from Interface Builder (or Xcode4) in your code.
Here's an easier fix:
self.backgroundColor = [UIColor clearColor];
I hesitate to suggest this because I don't want to offend you, but have you set opaque to NO?
This is easily reproducible:
Create a new View-Based iPhone Application. Create an UIView subclass and leave the drawRect: method completely empty. In Interface Builder, drag a UIView into the main view, assign it a background color and set the class of the view to your subclass. Save, build and run, and see, the view shows the background color.
I have circumvented this behaviour by overriding setBackgroundColor: and assigning the color to my own ivar, always leaving the backgroundColor property nil. This works, however I'm still wondering why this works this way.
You should set
clearsContextBeforeDrawing = NO
It's that simple.
From the UIView Reference Docs
Oops. I misunderstood the question. The OP (original poster) wants their custom control to support the standard "backgroundColor" property (eg, set by the gui designer), but does NOT want the system to paint that color. The OP wants to paint the bg himself so that he can round the corners.
In that case, the post about overriding the layer level drawing is correct.
The solution I posted will prevent the UI system from clearing your buffer before drawing. When you have no bg defined, if clearsContextBeforeDrawing is set, the iOS will clear your view's buffer, setting all pixels to transparent black. Doing this for a full-screen view takes about 5ms in an iPad3, so it's not free (pushing that 2048x1536 pixels never is). For comparison drawing a full-screen bitmap (using kCGBlendModeCopy to force blitting) takes ~25ms (using quartz, not GPU).
While it is possible to override the code that copies the background colour into the view's layer, I'm not a fan of this approach. I believe it can cause issues with views intended to be opaque. My suggestion is to create a custom colour variable like this:
#IBInspectable var foregroundColor: UIColor = .black {
didSet { setNeedsDisplay() }
}
Now, set your view's backgroundColor to clear and use foregroundColor in your drawing code in its place. The code above takes care of updating it when the value changes, and also exposes it to Interface Builder if required.
Another alternative is to use your view's tintColor instead, and to ensure that it updates correctly when tint colour changes. You may be reserving tint colour for other uses though, so this is not necessarily ideal. If you use tintColor, don't forget to include the following:
override func tintColorDidChange() {
setNeedsDisplay()
}