Drawing into transparent overlay subview NSView - macos

I've got a relatively simple Cocoa on Mac OS X 10.6 question to ask. I have a main NSView (ScreensaverView, actually) that is not layer backed and is otherwise unremarkable. It does some basic drawing in its drawRect via NSRectFill and NSBezierPath:stroke calls (dots and lines, basically).
I've also got a NSView-derived subclass that is acting as a child subview. I'm doing this with the goal to draw simple lines in the subview that draw on top of whatever is drawn in the main view, but then can be somehow "erased" revealing whatever the lines obscured. The code for this subview is quite simple:
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
}
return self;
}
- (void)dealloc
{
[super dealloc];
}
- (void)drawRect:(NSRect)dirtyRect {
// Transparent background
[[NSColor clearColor] set];
NSRectFillUsingOperation(dirtyRect, NSCompositeCopy);
// If needed for this update, draw line
if (drawLine) {
// OMITTED: Code that sets opaque NSColor and draws a line using NSBezierPath:stroke
}
// If needed for this update, "erase" line
if (eraseLine) {
[[NSColor blackColor] set]; // clearColor?
// OMITTED: Code that draws the same line using NSBezierPath:stroke
}
}
With the code as shown above, any time the subview draws, the main view goes black and you only see the subview line. When the subview isn't being updated, the main view contents appear again.
Things I've tried, with varying results:
I tried experimenting with making the subview return YES from an overridden isOpaque (which I realize isn't really correct). When I do this, both views draw properly during a subview update, however the subview line overwrites anything it is drawn on (ok) and then when erased, also leaves a large black line where it was (what I was trying to avoid). Trying to "erase" the line using clearColor instead of blackColor results in the line remaining on screen.
I tried making both (and/or just the subview) layer-backed views via calling [self setWantsLayer:YES] in init, and this results in a completely black screen.
I feel like I'm missing something really basic, but for whatever reason, I can't seem to figure it out. Any and all suggestions are greatly appreciated.

I think you are fundamentally misunderstanding how view drawing works. Basically, every time drawRect: is called on your view, the drawing commands in drawRect: are executed. This might happen automatically or when you issue the view a setNeedsDisplay: message.
Normally, when drawRect: is called on the view, the view is erased and drawing begins anew. Nothing remains in the view from the previous call to drawRect:. You do not need to fill the view with [NSColor clearColor] in order for it to be transparent.
Note that this is only the case if your view returns NO from isOpaque, which is the default. If your view is returning YES from isOpaque then you will need to ensure you erase the view before drawing, however in your particular case you should not return YES from isOpaque because you want your view to be transparent.
You do not need to "erase" the line you've drawn. It will be erased for you. Instead, you need to simply not draw it when you don't want it drawn.
Basically, your view should store some flag (such as your BOOL ivar named drawLine or something similar). Then, in your drawing code you should simply check if this value is set. If it is, you should draw the line. If not, then just do nothing. Your code should be reduced to this:
- (void)drawRect:(NSRect)rect
{
// If needed for this update, draw line
if (drawLine) {
// OMITTED: Code that sets opaque NSColor and draws a line using NSBezierPath:stroke
}
}
If you want to change the state and redraw the view, all you need to do is change the value of the drawLine ivar and ask the view to redraw using [yourView setNeedsDisplay:YES].
You can easily do this with a timer:
- (void)awakeFromNib
{
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(update:) userInfo:nil repeats:YES];
}
- (void)update:(NSTimer*)timer
{
drawLine = !drawLine;
[self setNeedsDisplay:YES];
}

Related

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.

Resizing MTKView scales old content before redraw

I'm using a MTKView to draw Metal content. It's configured as follows:
mtkView = MTKView(frame: self.view.frame, device: device)
mtkView.colorPixelFormat = .bgra8Unorm
mtkView.delegate=self
mtkView.sampleCount=4
mtkView.isPaused=true
mtkView.enableSetNeedsDisplay=true
setFrameSize is overriden to trigger a redisplay.
Whenever the view resizes it scales its old content before it redraws everything. This gives a jittering feeling.
I tried setting the contentGravity property of the MTKView's layer to a non-resizing value, but that totally messes up the scale and position of the content. It seems MTKView doesn't want me to fiddle with that parameter.
How can I make sure that during a resize the content is always properly redrawn?
In my usage of Metal and MTKView, I tried various combinations of presentsWithTransaction and waitUntilScheduled without success. I still experienced occasional frames of stretched content in between frames of properly rendered content during live resize.
Finally, I dropped MTKView altogether and made my own NSView subclass that uses CAMetalLayer and resize looks good now (without any use of presentsWithTransaction or waitUntilScheduled). One key bit is that I needed to set the layer's autoresizingMask to get the displayLayer method to be called every frame during window resize.
Here's the header file:
#import <Cocoa/Cocoa.h>
#interface MyMTLView : NSView<CALayerDelegate>
#end
Here's the implementation:
#import <QuartzCore/CAMetalLayer.h>
#import <Metal/Metal.h>
#implementation MyMTLView
- (id)initWithFrame:(NSRect)frame
{
if (!(self = [super initWithFrame:frame])) {
return self;
}
// We want to be backed by a CAMetalLayer.
self.wantsLayer = YES;
// We want to redraw the layer during live window resize.
self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
// Not strictly necessary, but in case something goes wrong with live window
// resize, this layer placement makes it more obvious what's going wrong.
self.layerContentsPlacement = NSViewLayerContentsPlacementTopLeft;
return self;
}
- (CALayer*)makeBackingLayer
{
CAMetalLayer* metalLayer = [CAMetalLayer layer];
metalLayer.device = MTLCreateSystemDefaultDevice();
metalLayer.delegate = self;
// *Both* of these properties are crucial to getting displayLayer to be
// called during live window resize.
metalLayer.autoresizingMask = kCALayerHeightSizable | kCALayerWidthSizable;
metalLayer.needsDisplayOnBoundsChange = YES;
return metalLayer;
}
- (CAMetalLayer*)metalLayer
{
return (CAMetalLayer*)self.layer;
}
- (void)setFrameSize:(NSSize)newSize
{
[super setFrameSize:newSize];
self.metalLayer.drawableSize = newSize;
}
- (void)displayLayer:(CALayer*)layer
{
// Do drawing with Metal.
}
#end
For reference, I do all my Metal drawing in MTKView's drawRect method.
I have the same problem with glitches on view resizing. You can even reproduce it in the HelloTriangle example from the Apple's developer site. However the effect is minimized because the triangle is drawn near the middle of the screen, and it's the content closest to the edge of the window, opposite the corner that drags, that is effected worst. The developer notes regarding use of presentsWithTransaction and waitUntilScheduled do not work for me either.
My solution was to add a Metal layer beneath the window.contentView.layer, and to make that layer large enough that it rarely needs to be resized. The reason this works is that, unlike the window.contentView.layer, which sizes itself automatically to the view (in turn maintaining the window size), you have explicit control of the sublayer size. This eliminates the flickering.
This helped me - https://github.com/trishume/MetalTest
He uses MetalLayer and careful setting of various properties. Everything is pretty smooth even with two side by side in synchronised scroll views with 45megapixel images.
A link to my original problem How do I position an image correctly in MTKView?

NSPopover below caret in NSTextView

I know that in order to show a popover I need an NSView, but I don't think that there is one associated with the caret (inside the NSTextView). Is there a way to show a NSPopover below the caret?
I tried to alloc a NSView and position it using (NSRect)boundingRectForGlyphRange:(NSRange)glyphRange inTextContainer:(NSTextContainer *)container, but the popover will not appear (and there's a reason, that method returns NSRect: {{0, 0}, {0, 0}}).
I'm not sure if you are still looking for answer. I recently was working on a project which happens to need a very similar feature like you described.
You can do the following inside a subclass of NSTextView:
the function you are going to call is : showRelativeToRect:ofView:preferredEdge:
the rect will be a rect inside the NSTextView, using the NSTextView coordinate system, the ofView is the NSTextView, and the preferredEdge is the edge you want this popover thing to hook with.
now, you are saying that you want the PopOver thing to show under the caret, well you have to give him a Rect, a Point is not enough. The NSTextView has a selector called selectedRange, which gives you the range of the selected text, you can use that to locate your caret.
the next thing is to call firstRectForCharacterRange (the class must delegate NSTextInputClient), this method will return a screen coordinate of the selected text inside the NSTextView, then you convert them into the NSTextView coordinate system, you will be able to show the NSPopover thing at a correct position. Here's my code of doing this.
NSRect rect = [self firstRectForCharacterRange:[self selectedRange]]; //screen coordinates
// Convert the NSAdvancedTextView bounds rect to screen coordinates
NSRect textViewBounds = [self convertRectToBase:[self bounds]];
textViewBounds.origin = [[self window] convertBaseToScreen:textViewBounds.origin];
rect.origin.x -= textViewBounds.origin.x;
rect.origin.y -= textViewBounds.origin.y;
rect.origin.y = textViewBounds.size.height - rect.origin.y - 10; //this 10 is tricky, if without, my control shows a little below the text, which makes it ugly.
NSLog(#"rect %#", NSStringFromRect(rect));
NSLog(#"bounds %#", NSStringFromRect([self bounds]));
if([popover isShown] == false)
[popover showRelativeToRect:rect
ofView:self preferredEdge:NSMaxYEdge];
and this is the result.
All the thing I am wondering is that if there is a way to convert using System functions, although I tried the convertRect:toView, but since this method is written in a delegate, the NSTextView always has the coordinate system of (0,0), which makes this method useless.

Drawing lines and points with Cocoa

I'm trying to draw some dots connected with lines. The dot consists of the "nucleus" with orbital area around it.
The problem appears when I try to move those dots which gives me distorted lines:
In my drawRect: method I iterate over an array of created dots and draw a bezier path by using lineToPoint: methods.
Dot *prevDot = nil;
NSBezierPath *line = [NSBezierPath bezierPath];
for (Dot *dot in _dots) {
if (!prevDot) {
[line moveToPoint:dot.position];
} else {
[line lineToPoint:dot.position];
}
prevDot = dot;
}
[line stroke];
My question is what technique should I use to implement clean line update between points once one of them is moved?
Your drawing code is correct. However, you must update a larger portion of the view when it changes. It looks like you're only updating the point which moves, and not the lines. An easy fix is to call:
[myView setNeedsDisplay:YES];
whenever you change the location of the point. This will redraw the entire view. You can use other methods to more selectively update the view only where it changes, which may give you better performance.
Typically, you'd call this in a method invoked from an NSNotification sent by your data class.

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