Subview of subclassed NSView re-positioned incorrectly after drawing a focus ring - macos

I wanted to create a focus ring outside a subclassed NSView to identify selection. My reference comes from here: Link.
I followed the reference, overwrote the -drawRect method as:
#property (nonatomic) BOOL shouldDisplayFocus;
...
- (void)drawRect:(NSRect)dirtyRect
{
// Drawing code here.
if (_shouldDisplayFocus)
{
[self setKeyboardFocusRingNeedsDisplayInRect:[self bounds]];
}
[super drawRect:dirtyRect];
[[NSColor blackColor] set];
NSRectFill(dirtyRect);
if (_shouldDisplayFocus)
{
NSSetFocusRingStyle(NSFocusRingTypeExterior);
NSBezierPath *path = [NSBezierPath bezierPathWithRect:NSInsetRect([self bounds], -1.0, -1.0)];
[[NSColor blackColor] set];
[path stroke];
[NSGraphicsContext restoreGraphicsState];
}
}
And its -mouseDown: method also overwritten:
- (void)mouseDown:(NSEvent *)theEvent
{
[super mouseDown:theEvent];
if (_delegate && [_delegate respondsToSelector:#selector(mouseDownAtView:withEvent:)])
{
[_delegate mouseDownAtView:self withEvent:theEvent];
}
}
And after the view is clicked, its delegate would set/un-set the focus ring and which would make its -drawRect: called again.
It worked and generated the focus ring outside the view correctly. However, one problem occurred soon:
I had an image view inside the subclassed view. As the image view rectangle was auto-layout with NSLayoutConstraint objects, I create four NSLayoutConstraint outlets to adjust their values. I do not frequently change the layout constraints. Actually, as the image size remained unchanged, I would not set them.
Here is the situation when the subclassed view not clicked (seemed fine):
Then click on the image (the focus ring generated, but...):
And I tried resize the window, things got even more sadly "FUNNY":
I could not understand why the problem is or how to solve that. Could anyone help me with that? I have uploaded my sample code here: Download
Quite sad that no one answer this question.
I noticed that the subviews also layouted incorrectly when they were add to this view by -addSubview: and -setFrame method.

Really late answer, but here it is anyway: you didn't call [NSGraphicsContext saveGraphicsState] at the start of the if (_shouldDisplayFocus) { block.
You call [NSGraphicsContext restoreGraphicsState] to pop the graphics state off the stack, but you never put anything on the stack. Cocoa is using the graphics state stack to draw everything so you are popping off some unknown state that has something to do with the position of the image. If you want to add the focus ring style and be able to remove the focus ring style you need to first save the graphics state, set the focus ring style to whatever you want, and then restore the graphics state back to what it was.

Related

NSTextField Drawing on Top of Sub View

I have created a secondary NSViewController to create a progress indicator "popup". The reason for this is that the software has to interact with some hardware and some of the functions take the device a few seconds to respond. So being thoughtful of the end user I have a NSViewController that has a NSView (that is black and semi-transparent) and then a message/progress bar on top. This is added to the window using addSubView.
Everything works great except when the screen has a NSTextField in it. The popup shows but the NSTextField is drawn on top. What is this?
The view code I used for drawing semi-transparent:
#implementation ConnectingView
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect
{
[super drawRect:dirtyRect];
// Drawing code here.
CGContextRef context = (CGContextRef) [[NSGraphicsContext currentContext] graphicsPort];
CGContextSetRGBFillColor(context, 0.227,0.251,0.337,0.8);
CGContextFillRect(context, NSRectToCGRect(dirtyRect));
}
#end
The code I use to show the progress view
-(void) showProgressWithMessage:(NSString *) message andIsIndet:(BOOL) indet
{
connectingView = [[ConnectingViewController alloc] init];
[self.view.window.contentView addSubview:connectingView.view];
connectingView.view.frame = ((NSView*)self.view.window.contentView).bounds;
[connectingView changeProgressLabel:message];
if (indet)
[connectingView makeProgressBar:NO];
}
Is there a better way to add the subview or to tell the NSTextFields I don't want them to be drawn on top?
Thanks!
So Setting [self setWantsLayer] to my custom NSViews sort of worked however there are a lot of redraw issues (white borders, and backgrounds). A NSPopover may be better in some instances however I was going for "locked down" approach where the interface is unreachable until it finishes (or times out).
What worked for me was to go to the instance of my NSView, select the window in Interface Builder, then go to layers (far right on properties view) and select my view under "Core Animation Layer".

NSTableView with rounded corners

I'm trying to customize the UI of my application and I want my NSTableView to have rounded corners. So I subclassed NSTableView and got this:
However, when I populate the table and select a row, the selection is drawn over the border like this:
I've tried adding a clip in the table view drawing code and it doesn't work. Any suggestions for how I can fix this?
Edit:
My drawing code in the NSTableView is the following:
- (void)drawRect:(NSRect)dirtyRect {
[NSGraphicsContext saveGraphicsState];
NSRect frame = NSMakeRect(0.0, 0.0, [self bounds].size.width, [self bounds].size.height-1.0);
[[NSBezierPath bezierPathWithRoundedRect:frame xRadius:3.6 yRadius:3.6] addClip];
[super drawRect:dirtyRect];
[NSGraphicsContext restoreGraphicsState];
}
The actual rounded frame is drawn in the NSScrollView drawRect method. The interesting thing is that this does clip the selection of the very first and very last rows:
But not when the table is scrolling:
So the question remains: how can I clip all drawing inside the rounded frame of the NSScrollView?
I found that you can call this on the container scroll view of the table view.
self.scrollView.wantsLayer = TRUE;
self.scrollView.layer.cornerRadius = 6;
That's all I needed and it works. No subclassing needed.
I was able to solve this pretty nicely using CALayer. After trying subclassing everything from NSScrollView to NSTableView to NSClipView, and still getting the rendering problems shown above, I finally simply added this code to the drawRect of the NSScrollView subclass:
if (!self.contentView.wantsLayer) {
[self.contentView setWantsLayer:YES];
[self.contentView.layer setCornerRadius:4.0f];
}
And then I draw the frame in the same drawRect method of the NSScrollView. It solves all the problems above.

Two Finger Drag with IKImageView and NSScrollView in Mountain Lion

I have a Mac App that's been in the app store for a year or so now. It was first published with target SDK 10.7, Lion. Upon the update to Mountain Lion it no longer works.
The application displays large images in an IKImageView which is embedded in an NSScrollView. The purpose of putting it into a scrollview was to get two finger dragging working, rather than the user having to click to drag. Using ScrollViewWorkaround by Nicholas Riley, I was able to use two finger scrolling to show the clipped content after the user had zoomed in. Just like you see in the Preview app.
Nicholas Riley's Solution:
IKImageView and scroll bars
Now in Mountain Lion this doesn't work. After zooming in, pinch or zoom button, the image is locked in the lower left portion of the image. It won't scroll.
So the question is, what's the appropriate way to display a large image in IKImageView and have two finger dragging of the zoomed image?
Thank you,
Stateful
Well, Nicholas Riley's Solution is an ugly hack in that it addresses the wrong class; the issue isn't with NSClipView (which he subclassed, but which works just fine as is), but with IKImageView.
The issue with IKImageView is actually quite simple (God knows why Apple hasn't fixed this in what? … 7 years ...): Its size does not adjust to the size of the image it displays. Now, when you embed an IKImageView in an NSScrollView, the scroll view obviously can only adjust its scroll bars relative to the size of the embedded IKImageView, not to the image it contains. And since the size of the IKImageView always stays the same, the scroll bars won't work as expected.
The following code subclasses IKImageView and fixes this behavior. Alas, it won't fix the fact that IKImageView is crash-prone in Mountain Lion as soon as you zoom …
///////////////////// HEADER FILE - FixedIKImageView.h
#import <Quartz/Quartz.h>
#interface FixedIKImageView : IKImageView
#end
///////////////////// IMPLEMENTATION FILE - FixedIKImageView.m
#import "FixedIKImageView.h"
#implementation FixedIKImageView
- (void)awakeFromNib
{
[self setTranslatesAutoresizingMaskIntoConstraints:NO]; // compatibility with Auto Layout; without this, there could be Auto Layout error messages when we are resized (delete this line if your app does not use Auto Layout)
}
// FixedIKImageView must *only* be used embedded within an NSScrollView. This means that setFrame: should never be called explicitly from outside the scroll view. Instead, this method is overwritten here to provide the correct behavior within a scroll view. The new implementation ignores the frameRect parameter.
- (void)setFrame:(NSRect)frameRect
{
NSSize imageSize = [self imageSize];
CGFloat zoomFactor = [self zoomFactor];
NSSize clipViewSize = [[self superview] frame].size;
// The content of our scroll view (which is ourselves) should stay at least as large as the scroll clip view, so we make ourselves as large as the clip view in case our (zoomed) image is smaller. However, if our image is larger than the clip view, we make ourselves as large as the image, to make the scrollbars appear and scale appropriately.
CGFloat newWidth = (imageSize.width * zoomFactor < clipViewSize.width)? clipViewSize.width : imageSize.width * zoomFactor;
CGFloat newHeight = (imageSize.height * zoomFactor < clipViewSize.height)? clipViewSize.height : imageSize.height * zoomFactor;
[super setFrame:NSMakeRect(0, 0, newWidth - 2, newHeight - 2)]; // actually, the clip view is 1 pixel larger than the content view on each side, so we must take that into account
}
//// We forward size affecting messages to our superclass, but add [self setFrame:NSZeroRect] to update the scroll bars. We also add [self setAutoresizes:NO]. Since IKImageView, instead of using [self setAutoresizes:NO], seems to set the autoresizes instance variable to NO directly, the scrollers would not be activated again without invoking [self setAutoresizes:NO] ourselves when these methods are invoked.
- (void)setZoomFactor:(CGFloat)zoomFactor
{
[super setZoomFactor:zoomFactor];
[self setFrame:NSZeroRect];
[self setAutoresizes:NO];
}
- (void)zoomImageToRect:(NSRect)rect
{
[super zoomImageToRect:rect];
[self setFrame:NSZeroRect];
[self setAutoresizes:NO];
}
- (void)zoomIn:(id)sender
{
[super zoomIn:self];
[self setFrame:NSZeroRect];
[self setAutoresizes:NO];
}
- (void)zoomOut:(id)sender
{
[super zoomOut:self];
[self setFrame:NSZeroRect];
[self setAutoresizes:NO];
}
- (void)zoomImageToActualSize:(id)sender
{
[super zoomImageToActualSize:sender];
[self setFrame:NSZeroRect];
[self setAutoresizes:NO];
}
- (void)zoomImageToFit:(id)sender
{
[self setAutoresizes:YES]; // instead of invoking super's zoomImageToFit: method, which has problems of its own, we invoke setAutoresizes:YES, which does the same thing, but also makes sure the image stays zoomed to fit even if the scroll view is resized, which is the most intuitive behavior, anyway. Since there are no scroll bars in autoresize mode, we need not add [self setFrame:NSZeroRect].
}
- (void)setAutoresizes:(BOOL)autoresizes // As long as we autoresize, make sure that no scrollers flicker up occasionally during live update.
{
[self setHasHorizontalScroller:!autoresizes];
[self setHasVerticalScroller:!autoresizes];
[super setAutoresizes:autoresizes];
}
#end

How to implement HUD-style window like Address Book's "Show in Large Type"

Several apps, including the built-in Address Book use a HUD window that is semi-transparent, with large shadowed text. I'd like to implement a similar window in my Cocoa Mac app.
Is there a free implementation of this kind of window somewhere?
If not, what is the best way to implement it?
Here's a sample project that shows how to do it:
http://github.com/NSGod/BlackBorderlessWindow
Basically, you need to create a borderless NSWindow subclass. The easiest way to do this is to set your window size and arrangement in the nib file, and then set its class to be your custom subclass. So while it will still look like a normal window in Interface Builder, at runtime it will appear as you need it to.
#implementation MDBorderlessWindow
- (id)initWithContentRect:(NSRect)contentRect
styleMask:(NSUInteger)windowStyle
backing:(NSBackingStoreType)bufferingType
defer:(BOOL)deferCreation {
if (self = [super initWithContentRect:contentRect
styleMask:NSBorderlessWindowMask
backing:NSBackingStoreBuffered defer:deferCreation]) {
[self setAlphaValue:0.75];
[self setOpaque:NO];
[self setExcludedFromWindowsMenu:NO];
}
return self;
}
The alpha value will make the window semi-transparent.
Also, you can create a custom NSView subclass that will draw a round rectangle:
#implementation MDBlackTransparentView
- (id)initWithFrame:(NSRect)frame {
if (self = [super initWithFrame:frame]) {
}
return self;
}
- (void)drawRect:(NSRect)frame {
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:frame
xRadius:6.0 yRadius:6.0];
[[NSColor blackColor] set];
[path fill];
}
#end
Like with the window, you simply set the class of the window's contentView to be your custom NSView subclass. (Use outline view mode and click the disclosure triangle to show the nested NSView inside the icon of the window in the nib file). Again, while the view will look ordinary in Interface Builder, it will look okay at runtime.
Then just place an NSTextField on top of view and set the text accordingly.
Note that, in general, borderless windows aren't easy to work with (for example, if you want to be able to drag the window around, you'll need to add that functionality back yourself). Apple has some sample code on how to allow dragging, for instance.
Thank you for sharing this code. Helped me a lot!
You may add the following line...
[self setBackgroundColor:[NSColor clearColor]];
to the init function of the window. This removes the white corners.

NSView Not updating?

Im working on a drag n' drop view and found some handlers for drag and drop actions on the web. I want to make it so it turns blue when the user drags a file over the drag and drop area and gray again when they exit the drag and drop area. The issues is its not updating when you drag your mouse over it or exit it. Heres some of the code:
- (void)drawRect:(NSRect)rect
{
NSRect bounds = [self bounds];
[[NSColor grayColor] set];
[NSBezierPath fillRect:bounds];
}
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender {
NSRect bounds = [self bounds];
[[NSColor blueColor] set];
[NSBezierPath fillRect:bounds];
return NSDragOperationCopy;
}
- (void)draggingExited:(id <NSDraggingInfo>)sender {
NSRect bounds = [self bounds];
[[NSColor grayColor] set];
[NSBezierPath fillRect:bounds];
}
Thanks for any help.
Are you calling [yourView: setNeedsDisplay] anywhere?
This is how you let the drawing framework know it needs to message your UIView subclass with drawRect:, so you should do it whenever things have changed. In your case, this probably means when the mouse enters or exits the drop area.
Drawing only works when a context (like a canvas for painting) is set up for you to draw into. When the framework calls -drawRect: it has set up a drawing context for you, so drawing commands like -[NSColor set] and -[NSBezierPath fillRect:] work as you expect.
Outside of -drawRect: there is usually no drawing context set up. Using drawing commands outside of -drawRect: is like waving a paintbrush in the air; there's no canvas, so no painting happens.
In 99.99% of cases, all view drawing should be kept within -drawRect: because NSView does a lot of work that you don't want to do to get the drawing context set up correctly and efficiently.
So, how do you change your view's drawing within your -draggingEntered: and -draggingExited: methods? By side effects.
You're doing the same thing in all three cases: 1) Setting a color and 2) Drawing a rectangle. The only difference is the color changes in each method. So, why not control which color you use in -drawRect: with an ivar, like so:
- (void)draggingEntered:(id <NSDraggingInfo>)sender {
drawBlueColorIvar = YES;
// ...
}
Then in -drawRect: you do this:
- (void)drawRect:(NSRect)rect {
NSColor *color = drawBlueColorIvar ? [NSColor blueColor] : [NSColor grayColor];
[color set];
[NSBezierPath fillRect:rect];
}
(Notice I didn't use [self bounds]. It is more efficient to just draw into the "dirty" rect, when possible.)
Finally, you need some way to tell the framework that your view needs to redraw when drawBlueColorIvar changes. The framework won't draw anything unless it's told it needs to. As Chris Cooper said, you do this with [self setNeedsDisplay:YES]. This should go after any place you change drawBlueColorIvar.

Resources