I'm learning Metal and Cocoa and trying to make a boilerplate application as a platform for future experiments. As part of the process, I'm implementing a view which will redraw itself (or, more accurately, contents of its CAMetalLayer) on 60fps. Also for educational purposes Im avoiding MTKView (for "learning Cocoa part"). Here's an abbreviated code snippet of how I'm tackling the problem:
#implementation MyMetalView // which is a subclass of NSView
- (BOOL) isOpaque {
return YES;
}
- (NSViewLayerContentsRedrawPolicy) layerContentsRedrawPolicy {
return NSViewLayerContentsRedrawOnSetNeedsDisplay;
}
- (CALayer *) makeBackingLayer {
// create CAMetalLayer with default device
}
- (BOOL) wantsLayer {
return YES;
}
- (BOOL) wantsUpdateLayer {
return YES;
}
- (void) displayLayer:(CALayer *)layer {
id<MTLCommandBuffer> cmdBuffer = [_commandQueue commandBuffer];
id<CAMetalDrawable> drawable = [((CAMetalLayer *) layer) nextDrawable];
[cmdBuffer enqueue];
[cmdBuffer presentDrawable:drawable];
// rendering
[cmdBuffer commit];
}
#end
int main() {
// init app, window and MyMetalView instance
// invocation will call [myMetalViewInstance setNeedsDisplay:YES]
[NSTimer scheduledTimerWithTimeInterval:1./60. invocation:setNeedsDisplayInvokation repeats:YES];
[NSApp run];
return 0;
}
Is it the right way to do what I want? Or have I chosen a long and not recommended approach?
It is strongly preferred to use CVDisplayLink rather than a generic NSTimer to drive animations that need to match the refresh rate of the display.
You'll want to create an ivar or property to hold a CVDisplayLinkRef:
CVDisplayLinkRef displayLink;
Then, when your view is going onto the screen and you want to start animating, you'll create, configure, and start your display link:
CVDisplayLinkCreateWithActiveCGDisplays(&displayLink);
CVDisplayLinkSetOutputCallback(displayLink, &MyDisplayLinkCallback, self);
CVDisplayLinkStart(displayLink);
The display link callback should be a static function. It will be invoked at the beginning of the display's v-blank period (on modern displays where there is no physical v-blank, this still happens at a regular 60Hz cadence):
static CVReturn MyDisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp* now, const CVTimeStamp* outputTime, CVOptionFlags flagsIn, CVOptionFlags* flagsOut, void* displayLinkContext)
{
[(MyMetalView *)displayLinkContext setNeedsDisplay:YES];
return kCVReturnSuccess;
}
When your view leaves the display, or if you want to pause, you can release the display link and nil it out:
CVDisplayLinkRelease(displayLink);
following #warrenm solution adding dispatch_sync to refresh and other minor :
#import "imageDrawer.h"
#import "image/ImageBuffer.h"
#import "common.hpp"
#implementation imageDrawer {
CVDisplayLinkRef displayLink;
}
CVReturn MyDisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp* now, const CVTimeStamp* outputTime, CVOptionFlags flagsIn, CVOptionFlags* flagsOut, void* displayLinkContext)
{
dispatch_sync(dispatch_get_main_queue(), ^{
[(__bridge imageDrawer*)displayLinkContext setNeedsDisplay:YES];
});
return kCVReturnSuccess;
}
-(void)setContDisplay {
CVDisplayLinkCreateWithActiveCGDisplays(&displayLink);
CVDisplayLinkSetOutputCallback(displayLink, &MyDisplayLinkCallback, (__bridge void*)self);
CVDisplayLinkStart(displayLink);
}
-(void)awakeFromNib {
[self setContDisplay];
}
- (void)drawRect:(NSRect)rect {
[super drawRect:rect];
int w=rect.size.width, h=rect.size.height;
// do the drawing...
}
#end
Related
I ma trying to create a simple slide menu.
I created an NSView and Applied Gaussian Blurr to it's CALayer
//Menu initWithFrame method.
m_blur = [CIFilter filterWithName:#"CIGaussianBlur"];
[m_blur setDefaults];
[m_blur setValue: [NSNumber numberWithFloat:7] forKey:#"inputRadius"];
m_backgroundFiltersArray = [NSArray arrayWithObject:m_blur];
- (void)setVisible:(BOOL)bVisible
{
#autoreleasepool
{
if (!bVisible)
{
[self.layer setBackgroundFilters:nil];
[self.layer.superlayer setBackgroundFilters:nil];
}
else
{
[self.layer setBackgroundFilters:m_backgroundFiltersArray];
//Also Update the Driver's Parameters here.
}
}
[self setNeedsDisplay:YES];
}
In my App main View I am calling the menu like this
In init with Frame
m_menuView = [[MenuView alloc]initWithFrame:NSMakeRect(-fcMenuWidth, 0, fcMenuWidth, 446.0)];
[m_menuView setDelegate:self];
[self addSubview:m_menuView];
- (void)summonMenu
{
if(!m_bMenuUp)
{
m_bMenuUp = YES;
[m_menuView setVisible:YES];
[[m_menuView animator] setFrame:NSMakeRect(0, 0, fcMenuWidth, self.frame.size.height)];
}
}
- (void)hideMenu
{
if (m_bMenuUp)
{
m_bMenuUp = NO;
[m_menuView cancelPopUpButtons];
[[m_menuView animator] setFrame:NSMakeRect(-fcMenuWidth, 0, fcMenuWidth, self.frame.size.height)];
[m_menuView setVisible:NO];
}
}
However Each time I hide or summon the Menu
I have some little memory leak.
I tried to profile it in Instruments -- it tells about OpenGL context
That has about 768bytes leak each time.
I played around with functions
And discovered that when I am calling m_menuView setFrame: without the .animator property it all works fine and no leaks occurs.
Also if I disable CIFilter and use animator it works fine.
Can someone enlighten me how can I work this round please?
I have an NSWindow that has a drop shadow, but it is way to dark. The shadow spreads too far and is too heavy for me. It's the default shadow for the NSWindow and I haven't edited it at all.
What I want to know is if there is a way to shorten the blur radius or lower the heaviness of the drop shadow so it appears a bit more subtle.
Thanks!
There's no public API, but you can do It by swizzling some methods on NSThemeFrame (this is the view class responsible for the window's frame, border, etc).
Here's an example ( a subclass of NSWindow):
#import <objc/runtime.h>
#interface SOWindow : NSWindow
#end
#interface SOWindowThemeFrameOverrides : NSView
#end
#implementation SOWindow
+ (void)load
{
NSArray *methodsToOverride = #[#"_shadowOffset", #"_shadowFlags", #"_shadowType"];
for (NSString *selector in methodsToOverride) {
Method m = class_getInstanceMethod(NSClassFromString(#"NSThemeFrame"), NSSelectorFromString(selector));
Method m2 = class_getInstanceMethod([SOWindowThemeFrameOverrides class], NSSelectorFromString(selector));
class_addMethod(NSClassFromString(#"NSThemeFrame"), NSSelectorFromString([NSString stringWithFormat:#"_original%#", selector]), method_getImplementation(m), method_getTypeEncoding(m));
method_exchangeImplementations(m, m2);
}
}
#end
#implementation SOWindowThemeFrameOverrides
- (NSSize)_shadowOffset
{
if ([self.window isKindOfClass:[SOWindow class]]) {
return NSMakeSize(0, 8);
} else {
return [self _original_shadowOffset];
}
}
- (NSUInteger)_shadowFlags
{
if ([self.window isKindOfClass:[SOWindow class]]) {
return 0;
} else {
return [self _original_shadowFlags];
}
}
- (NSInteger)_shadowType
{
if ([self.window isKindOfClass:[SOWindow class]]) {
return 4;
} else {
return [self _original_shadowType];
}
}
#pragma mark Placeholder methods
- (NSSize)_original_shadowOffset
{
// implementation will be filled in at runtime
return NSZeroSize;
}
- (NSUInteger)_original_shadowFlags
{
// implementation will be filled in at runtime
return 0;
}
- (NSInteger)_original_shadowType
{
// implementation will be filled in at runtime
return 0;
}
#end
When the SOWindow class is loaded by the runtime, the + load method is invoked. The method switches NSThemeFrame's implementation of the 3 shadow methods by their implementation in SOWindowThemeFrameOverrides, also adding the original methods to the class with the _original prefix.
When the swizzled methods are called, we check to see if the window is a SOWindow, if It is we use the custom shadow, if It's not we forward the call to the original implementations.
This is what I get by returning 4 from _shadowType:
Please note that this is a huge hack and would probably be rejected if you tried to submit It to the AppStore.
My app can open the type of file it is supposed to work with when I double click the file, AND the app is already running. However, when the app is not running yet and i double click a file, the app starts, but it does not open the file. Why could that be?
The app delegate implements the methods:
-(void) application:(NSApplication *)sender openFiles:(NSArray *)filenames {
for (NSString *name in filenames) {
NSLog(#"Openning files");
[self.topController addFileAtPath:name];
}
}
-(BOOL) application:(NSApplication *)sender openFile:(NSString *)filename {
NSLog(#"Openning file_");
[self.topController addFileAtPath:filename];
return YES;
}
For those who might fall into the same trap:
Turns out, the methods above get called earlier than the "-applicationDidFinishLaunching:", in which I was doing all my app initialisation. I ended up creating an "alive" flag (to show if my app has been inited yet), and put all my initialization logic in a separate method. Then, in my "...finishedLaunching", "openFiles" and "openFile" i check whether that flag is on or off, and call the application initialization method accordingly:
#implementation DTVAppDelegate
BOOL alive = NO;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
if (!alive) {
[self startApp];
}
}
- (void) startApp {
// init logic
alive = YES;
}
-(void) application:(NSApplication *)sender openFiles:(NSArray *)filenames {
if (!alive) {
[self startApp];
}
for (NSString *name in filenames) {
NSLog(#"Openning files");
[self.topController addFileAtPath:name];
}
}
I'm making a little server app for OS X and I'm using an NSTextView to log some info about connected clients.
Whenever I need to log something I'm appending the new message to the text of the NSTextView this way:
- (void)logMessage:(NSString *)message
{
if (message) {
self.textView.string = [self.textView.string stringByAppendingFormat:#"%#\n",message];
}
}
After this I'd like the NSTextField (or maybe I should say the NSClipView that contains it) to scroll down to show the last line of its text (obviously it should scroll only if the last line is not visible yet, in fact if then new line is the first line I log it is already on the screen so there is no need to scroll down).
How can I do that programmatically?
Found solution:
- (void)logMessage:(NSString *)message
{
if (message) {
[self appendMessage:message];
}
}
- (void)appendMessage:(NSString *)message
{
NSString *messageWithNewLine = [message stringByAppendingString:#"\n"];
// Smart Scrolling
BOOL scroll = (NSMaxY(self.textView.visibleRect) == NSMaxY(self.textView.bounds));
// Append string to textview
[self.textView.textStorage appendAttributedString:[[NSAttributedString alloc]initWithString:messageWithNewLine]];
if (scroll) // Scroll to end of the textview contents
[self.textView scrollRangeToVisible: NSMakeRange(self.textView.string.length, 0)];
}
As of OS 10.6 it's as simple as nsTextView.scrollToEndOfDocument(self).
Swift 4 + 5
let smartScroll = self.textView.visibleRect.maxY == self.textView.bounds.maxY
self.textView.textStorage?.append("new text")
if smartScroll{
self.textView.scrollToEndOfDocument(self)
}
I've been messing with this for a while, because I couldn't get it to work reliably. I've finally gotten my code working, so I'd like to post it as a reply.
My solution allows you to scroll manually, while output is being added to the view. As soon as you scroll to the absolute bottom of the NSTextView, the automatic scrolling will resume (if enabled, that is).
First a category to #import this only when needed...
FSScrollToBottomExtensions.h:
#interface NSView (FSScrollToBottomExtensions)
- (float)distanceToBottom;
- (BOOL)isAtBottom;
- (void)scrollToBottom;
#end
FSScrollToBottomExtensions.m:
#implementation NSView (FSScrollToBottomExtensions)
- (float)distanceToBottom
{
NSRect visRect;
NSRect boundsRect;
visRect = [self visibleRect];
boundsRect = [self bounds];
return(NSMaxY(visRect) - NSMaxY(boundsRect));
}
// Apple's suggestion did not work for me.
- (BOOL)isAtBottom
{
return([self distanceToBottom] == 0.0);
}
// The scrollToBottom method provided by Apple seems unreliable, so I wrote this one
- (void)scrollToBottom
{
NSPoint pt;
id scrollView;
id clipView;
pt.x = 0;
pt.y = 100000000000.0;
scrollView = [self enclosingScrollView];
clipView = [scrollView contentView];
pt = [clipView constrainScrollPoint:pt];
[clipView scrollToPoint:pt];
[scrollView reflectScrolledClipView:clipView];
}
#end
... create yourself an "OutputView", which is a subclass of NSTextView:
FSOutputView.h:
#interface FSOutputView : NSTextView
{
BOOL scrollToBottomPending;
}
FSOutputView.m:
#implementation FSOutputView
- (id)setup
{
...
return(self);
}
- (id)initWithCoder:(NSCoder *)aCoder
{
return([[super initWithCoder:aCoder] setup]);
}
- (id)initWithFrame:(NSRect)aFrame textContainer:(NSTextContainer *)aTextContainer
{
return([[super initWithFrame:aFrame textContainer:aTextContainer] setup]);
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super dealloc];
}
- (void)awakeFromNib
{
NSNotificationCenter *notificationCenter;
NSView *view;
// viewBoundsDidChange catches scrolling that happens when the caret
// moves, and scrolling caused by pressing the scrollbar arrows.
view = [self superview];
[notificationCenter addObserver:self
selector:#selector(viewBoundsDidChangeNotification:)
name:NSViewBoundsDidChangeNotification object:view];
[view setPostsBoundsChangedNotifications:YES];
// viewFrameDidChange catches scrolling that happens because text
// is inserted or deleted.
// it also catches situations, where window resizing causes changes.
[notificationCenter addObserver:self
selector:#selector(viewFrameDidChangeNotification:)
name:NSViewFrameDidChangeNotification object:self];
[self setPostsFrameChangedNotifications:YES];
}
- (void)handleScrollToBottom
{
if(scrollToBottomPending)
{
scrollToBottomPending = NO;
[self scrollToBottom];
}
}
- (void)viewBoundsDidChangeNotification:(NSNotification *)aNotification
{
[self handleScrollToBottom];
}
- (void)viewFrameDidChangeNotification:(NSNotification *)aNotification
{
[self handleScrollToBottom];
}
- (void)outputAttributedString:(NSAttributedString *)aAttributedString
flags:(int)aFlags
{
NSRange range;
BOOL wasAtBottom;
if(aAttributedString)
{
wasAtBottom = [self isAtBottom];
range = [self selectedRange];
if(aFlags & FSAppendString)
{
range = NSMakeRange([[self textStorage] length], 0);
}
if([self shouldChangeTextInRange:range
replacementString:[aAttributedString string]])
{
[[self textStorage] beginEditing];
[[self textStorage] replaceCharactersInRange:range
withAttributedString:aAttributedString];
[[self textStorage] endEditing];
}
range.location += [aAttributedString length];
range.length = 0;
if(!(aFlags & FSAppendString))
{
[self setSelectedRange:range];
}
if(wasAtBottom || (aFlags & FSForceScroll))
{
scrollToBottomPending = YES;
}
}
}
#end
... You can add a few more convenience methods to this class (I've stripped it down), so that you can output a formatted string.
- (void)outputString:(NSString *)aFormatString arguments:(va_list)aArguments attributeKey:(NSString *)aKey flags:(int)aFlags
{
NSMutableAttributedString *str;
str = [... generate attributed string from parameters ...];
[self outputAttributedString:str flags:aFlags];
}
- (void)outputLineWithFormat:(NSString *)aFormatString, ...
{
va_list args;
va_start(args, aFormatString);
[self outputString:aFormatString arguments:args attributeKey:NULL flags:FSAddNewLine];
va_end(args);
}
I have some customised NSTextView and custom input method so my option was to use:
self.scrollView.contentView.scroll(NSPoint(x: 1, y: self.textView.frame.size.height))
I'm subclassing NSButton because I need to repeat a selector while the mouse is being held down.
I'm doing that like this:
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
[self setBezelStyle:NSBezelBorder];
PotRightIsDown = NO;
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect
{
// Drawing code here.
}
- (void)mouseDown:(NSEvent *)theEvent;
{
NSLog(#"pot right mouse down");
PotRightIsDown = YES;
holdDownTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(sendCommand) userInfo:nil repeats:YES];
}
- (void)mouseUp:(NSEvent *)theEvent;
{
NSLog(#"pot right mouse up");
PotRightIsDown = NO;
}
-(void)sendCommand
{
if (PotRightIsDown)
{
NSLog(#"run the stuff here");
}
else
{
[holdDownTimer invalidate];
}
}
Works like a champ, sends the command every 100ms.
In the window in IB, I've dragged a Bevel Button onto the window and set it's class to this subclass. When I ran the application, the button is invisible however it works. I'm guessing this is because I have an empty drawRect function in the subclass.
How can I make this subclassed button look like a Bevel button?
Thank you,
Stateful
If you aren't adding any functionality to a particular subclass method then you can simply avoid implementing it altogether, which will allow the superclass to provide the default behaviour.
Alternatively (as pointed out my #Carl Norum) you can explicitly do that using:
- (void)drawRect:(NSRect)dirtyRect
{
[super drawRect:dirtyRect];
}
But it's a bit pointless.