I am currently trying to create a simple cocoa NSWindow programmatically instead of using Interface builder (I have got my reasons to do so). this is a quick test:
int main(int argc, char** argv){
NSWindow *mainwin;
CocoaGLView *mainview;
NSRect scr_frame;
unsigned int style_mask;
NSAutoreleasePool *pool =[[NSAutoreleasePool alloc] init];
[NSApplication sharedApplication];
scr_frame= NSMakeRect(100, 100, 400, 400);
style_mask=NSClosableWindowMask|NSMiniaturizableWindowMask|
NSResizableWindowMask|NSTitledWindowMask;
scr_frame=[NSWindow contentRectForFrameRect:scr_frame
styleMask:style_mask];
mainwin=[[NSWindow alloc]
initWithContentRect:scr_frame
styleMask:style_mask
backing:NSBackingStoreBuffered
defer:NO];
[mainwin makeKeyAndOrderFront:nil];
[mainwin setTitle:#"Visible screen window"];
mainview=[[CocoaGLView alloc] initWithFrame:scr_frame];
[mainwin setContentView:mainview];
[mainview display];
[mainwin setReleasedWhenClosed:YES];
[pool drain];
[NSApp run];
return 0;
}
CocoaGLView is derived from NSOpenGLView and looks like this:
#interface CocoaGLView : NSOpenGLView {
//some stuff
}
- (id) initWithFrame: (NSRect) frameRect;
- (void)setFrameSize:(NSSize) aSize;
- (void)drawRect:(NSRect) aRect;
#end
it generally works. I can see the window. I can even see the openGL things I draw inside CocoaGLViews drawRect function, but that function unfortunatelly only gets called once, what am I missing?
Why would you expect it to be called more than once? A view is asked to draw when the OS thinks that its content is no longer valid. If you want the OpenGL view to be drawn periodically, then you need to set up a timer that sends setNeedsDisplay: messages to your view.
Related
So I now have my Auto Layout-based container working, for the most part. On 10.8 (I need to run on 10.7 and newer), I see this:
Notice how the sides of the NSProgressIndicator and NSPopUpButton are clipped.
After some experimentation, I found that overriding alignmentRectInsets and returning 50 pixels of insets on all sides shows no clipping:
In both cases, the controls are bound to the left and right edges of the container view alignment rect with H:|[view]|. I imagine this will happen on other versions of OS X too, but it's most noticeable here (and as of writing I only have access to 10.8 and 10.10 installs).
Now, using alignment rect insets of 50 pixels on each side sounds wrong. I don't think there'd be any control that would need more than 50 pixels, but I'd rather do these correctly. So my question is: How do I implement the alignmentRectForFrame: and frameForAlignmentRect: selectors to properly account for the frames and alignment rects of the subviews?
Right now, I'm thinking to force a layout and then observe the frames and alignment rects of each subview, assuming that alignment rect (0, 0) of my last subview (the subviews are arranged linearly) will be at alignment rect (0, 0) of the container view. But I'm not sure if this approach is sufficient to handle all cases, and I'm not sure if I can invert the operation in the same way that these two selectors require. Subtraction, maybe?
If what I described above is the solution, could I do that with alignmentRectInsets, or must the insets returned by that method never change during the lifetime of the view?
Or is the second screenshot showing a scenario that Interface Builder won't reproduce, and thus I assume is "wrong" from a guidelines standpoint?
In the sample program below, start without a command-line argument to simulate the first screenshot, and start with an argument to simulate the second screenshot. Check the Spaced checkbox to add spacing to the views.
Thanks!
// 17 august 2015
#import <Cocoa/Cocoa.h>
BOOL useInsets = NO;
#interface ContainerView : NSView
#end
#implementation ContainerView
- (NSEdgeInsets)alignmentRectInsets
{
if (useInsets)
return NSEdgeInsetsMake(50, 50, 50, 50);
return [super alignmentRectInsets];
}
#end
NSWindow *mainwin;
NSView *containerView;
NSProgressIndicator *progressbar;
NSPopUpButton *popupbutton;
NSButton *checkbox;
void addConstraints(NSView *view, NSString *constraint, NSDictionary *views)
{
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:constraint
options:0
metrics:nil
views:views];
[view addConstraints:constraints];
}
void relayout(BOOL spaced)
{
[containerView removeConstraints:[containerView constraints]];
NSDictionary *views = #{
#"pbar": progressbar,
#"pbutton": popupbutton,
#"checkbox": checkbox,
};
NSString *vconstraint = #"V:|[pbar][pbutton][checkbox]|";
if (spaced)
vconstraint = #"V:|[pbar]-[pbutton]-[checkbox]|";
addConstraints(containerView, vconstraint, views);
addConstraints(containerView, #"H:|[pbar]|", views);
addConstraints(containerView, #"H:|[pbutton]|", views);
addConstraints(containerView, #"H:|[checkbox]|", views);
NSView *contentView = [mainwin contentView];
[contentView removeConstraints:[contentView constraints]];
NSString *base = #":|[view]|";
if (spaced)
base = #":|-[view]-|";
views = #{
#"view": containerView,
};
addConstraints(contentView, [#"H" stringByAppendingString:base], views);
addConstraints(contentView, [#"V" stringByAppendingString:base], views);
}
#interface appDelegate : NSObject<NSApplicationDelegate>
#end
#implementation appDelegate
- (IBAction)onChecked:(id)sender
{
relayout([checkbox state] == NSOnState);
}
- (void)applicationDidFinishLaunching:(NSNotification *)note
{
mainwin = [[NSWindow alloc]
initWithContentRect:NSMakeRect(0, 0, 320, 240)
styleMask:(NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask | NSResizableWindowMask)
backing:NSBackingStoreBuffered
defer:YES];
NSView *contentView = [mainwin contentView];
containerView = [[ContainerView alloc] initWithFrame:NSZeroRect];
[containerView setTranslatesAutoresizingMaskIntoConstraints:NO];
progressbar = [[NSProgressIndicator alloc] initWithFrame:NSZeroRect];
[progressbar setControlSize:NSRegularControlSize];
[progressbar setBezeled:YES];
[progressbar setStyle:NSProgressIndicatorBarStyle];
[progressbar setIndeterminate:NO];
[progressbar setTranslatesAutoresizingMaskIntoConstraints:NO];
[containerView addSubview:progressbar];
popupbutton = [[NSPopUpButton alloc] initWithFrame:NSZeroRect];
[popupbutton setPreferredEdge:NSMinYEdge];
NSPopUpButtonCell *pbcell = (NSPopUpButtonCell *) [popupbutton cell];
[pbcell setArrowPosition:NSPopUpArrowAtBottom];
[popupbutton addItemWithTitle:#"Item 1"];
[popupbutton addItemWithTitle:#"Item 2"];
[popupbutton setTranslatesAutoresizingMaskIntoConstraints:NO];
[containerView addSubview:popupbutton];
checkbox = [[NSButton alloc] initWithFrame:NSZeroRect];
[checkbox setTitle:#"Spaced"];
[checkbox setButtonType:NSSwitchButton];
[checkbox setBordered:NO];
[checkbox setFont:[NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSRegularControlSize]]];
[checkbox setTarget:self];
[checkbox setAction:#selector(onChecked:)];
[checkbox setTranslatesAutoresizingMaskIntoConstraints:NO];
[containerView addSubview:checkbox];
[contentView addSubview:containerView];
relayout(NO);
[mainwin cascadeTopLeftFromPoint:NSMakePoint(20, 20)];
[mainwin makeKeyAndOrderFront:mainwin];
}
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)app
{
return YES;
}
#end
int main(int argc, char *argv[])
{
useInsets = (argc > 1);
NSApplication *app = [NSApplication sharedApplication];
[app setActivationPolicy:NSApplicationActivationPolicyRegular];
[app setDelegate:[appDelegate new]];
[app run];
return 0;
}
I have subclassed NSWindow and I have a MYWindow class implementing the following method:
-(void)resetCursorRects {
NSImage *image = [NSImage imageNamed:#"cursor.png"];
[image setSize:NSMakeSize(32, 32)];
NSCursor *cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSMakePoint(1, 1)];
[super resetCursorRects];
[self addCursorRect:[self bounds] cursor:cursor];
}
This will change the cursor for the whole window and I will see cursor.png instead of the default mouse pointer. The problem is that this only works if the MYWindow is set to the key window which is of course non trivial to make it.
In the beginning of my project I just have one main window but now I need to have two different MYWindow. The problem with two windows it is not possible to set both as the key window and hence the custom mouse pointer is only displayed on the active window. I need to click the other window to make the cursor appear.
Is there any way around this? So I get a custom cursor on both windows?
Edit: Tried NSTrackingArea
I added this to my content view's init method:
self.trackingArea = [[NSTrackingArea alloc] initWithRect:[self frame] options: (NSTrackingCursorUpdate | NSTrackingActiveAlways | NSTrackingMouseMoved) owner:self userInfo:nil];
[self addTrackingArea:self.trackingArea];
Then I overrided cursorUpdate: like this:
-(void)cursorUpdate:(NSEvent *)event {
NSLog(#"event : %#", event);
[[NSCursor crosshairCursor] set];
}
This makes the crosshairCursor show when the NSWindow that contains the NSImageView derived class is key window. But if I make another NSWindow within the app the key window, the cursor returns to the standard cursor again. Am I doing something wrong?
I struggled with this problem for a long period of time and I think there is only one way to change mouse cursor over inactive application (over non-foreground window). This is hacky and magic way.
Before calling pretty standard:
[[NSCursor pointingHandCursor] push];
You have to call:
void CGSSetConnectionProperty(int, int, int, int);
int CGSCreateCString(char *);
int CGSCreateBoolean(BOOL);
int _CGSDefaultConnection();
void CGSReleaseObj(int);
int propertyString, boolVal;
propertyString = CGSCreateCString("SetsCursorInBackground");
boolVal = CGSCreateBoolean(TRUE);
CGSSetConnectionProperty(_CGSDefaultConnection(), _CGSDefaultConnection(), propertyString, boolVal);
CGSReleaseObj(propertyString);
CGSReleaseObj(boolVal);
Or if you are using Swift:
Put this in your YourApp-Bridging-Header.h:
typedef int CGSConnectionID;
CGError CGSSetConnectionProperty(CGSConnectionID cid, CGSConnectionID targetCID, CFStringRef key, CFTypeRef value);
int _CGSDefaultConnection();
And then call:
let propertyString = CFStringCreateWithCString(kCFAllocatorDefault, "SetsCursorInBackground", 0)
CGSSetConnectionProperty(_CGSDefaultConnection(), _CGSDefaultConnection(), propertyString, kCFBooleanTrue)
You should be able to add an NSTrackingArea that changes the cursor, as long as you don’t want it to also change when the app is inactive (that is essentially impossible).
Edit:
I was able to get this working with the following code:
- (vod)someSetup;
{
NSTrackingArea *const trackingArea = [[NSTrackingArea alloc] initWithRect:NSZeroRect options: (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingInVisibleRect) owner:self userInfo:nil];
[self.view addTrackingArea:trackingArea];
}
- (void)mouseEntered:(NSEvent *)theEvent;
{
[[NSCursor IBeamCursor] push];
}
- (void)mouseExited:(NSEvent *)theEvent;
{
[[NSCursor IBeamCursor] pop];
}
Now I finally found a solution that works. I don't know if this will bite me in the tail in the future but at least this seem to work when testing.
Thanks Wil for the example it got me half way there. But it was only when I finally combined it with resetCursorRects and also defined a cursor rect in each view with the specific cursor. This took me a long time to figure out and I don't know if the solution is optimal (suggestions of improvement are welcome)
Below is the full example that made it work for me in the end (self.cursor is an instance of the cursor)
- (void)viewWillMoveToWindow:(NSWindow *)newWindow {
NSTrackingArea *const trackingArea = [[NSTrackingArea alloc] initWithRect:NSZeroRect options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect) owner:self userInfo:nil];
[self addTrackingArea:trackingArea];
[self.window invalidateCursorRectsForView:self];
}
- (void)resetCursorRects {
[super resetCursorRects];
[self addCursorRect:self.bounds cursor:self.cursor];
}
- (void)mouseEntered:(NSEvent *)theEvent {
[super mouseEntered:theEvent];
[self.cursor push];
}
- (void)mouseExited:(NSEvent *)theEvent {
[super mouseExited:theEvent];
[self.cursor pop];
}
I have an NSOpenGLView and OpenGL code that works with an NSTimer running in the main loop (calling setNeedsDisplay and drawRect). I would like to use a CVDisplayLink, so I can get a better frame-rate without overdriving the timer. I copied most of the code from apple's OSXGLEssentials example. The display link starts and the callback runs, but nothing is actually draw on screen. glGetError returns GL_INVALID_FRAMEBUFFER_OPERATION.
glCheckFramebufferStatus returns GL_FRAMEBUFFER_UNDEFINED for GL_FRAMEBUFFER, GL_DRAW_FRAMEBUFFER and GL_READ_FRAMEBUFFER.
Info from the documentation:
GL_FRAMEBUFFER_UNDEFINED is returned if target is the default
framebuffer, but the default framebuffer does not exist.
Here are the relevant bits of code:
- (void)awakeFromNib {
NSOpenGLPixelFormatAttribute attributes[] = {
NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core, // Core Profile !
NSOpenGLPFADoubleBuffer,
NSOpenGLPFAAccelerated,
NSOpenGLPFAColorSize, 24,
NSOpenGLPFAAlphaSize, 8,
NSOpenGLPFAAllowOfflineRenderers,
0
};
NSOpenGLPixelFormat *format = [[NSOpenGLPixelFormat alloc] initWithAttributes:attributes];
NSOpenGLContext *context = [[NSOpenGLContext alloc] initWithFormat:format shareContext: nil];
// [context setView: self];
[self setPixelFormat: format];
[self setOpenGLContext: context];
}
- (void)prepareOpenGL {
[super prepareOpenGL];
NSOpenGLContext* context = [self openGLContext];
[context makeCurrentContext];
// Synchronize buffer swaps with vertical refresh rate
GLint swapInt = 1;
[context setValues:&swapInt forParameter:NSOpenGLCPSwapInterval];
MyDisplay_setup();
MyDisplay_initScene(_bounds.size.width, _bounds.size.height);
CVDisplayLinkCreateWithActiveCGDisplays(&displayLink);
CVDisplayLinkSetOutputCallback(displayLink, &displayLinkCallback, (__bridge void *)self);
CGLPixelFormatObj cglPixelFormat = [[self pixelFormat] CGLPixelFormatObj];
CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(displayLink, [context CGLContextObj], cglPixelFormat);
CVDisplayLinkStart(displayLink);
}
static CVReturn displayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp* now, const CVTimeStamp* outputTime,
CVOptionFlags flagsIn, CVOptionFlags* flagsOut, void* displayLinkContext) {
#autoreleasepool {
[(__bridge MyView*)displayLinkContext redraw];
}
return kCVReturnSuccess;
}
- (void)redraw {
NSOpenGLContext* context = [self openGLContext];
[context makeCurrentContext];
CGLLockContext([context CGLContextObj]);
MyDisplay_drawScene();
CGLFlushDrawable([context CGLContextObj]);
CGLUnlockContext([context CGLContextObj]);
}
This is an old question, but this problem still persists, so here's my answer. For reference, i don't use Xcode at all, i write code in Vim and compile with Clang, so this is the default behaviour, and nothing to do with the IB. I use only the NSOpenGLView, NSOpenGLContext, and CGDisplayLink for rendering. I have a MacBook Pro (Retina, 15-inch, Mid 2014) running macOS Sierra.
While debugging i found that NSOpenGLContext's view property returned nil for the first few frames after starting the display link. This was enough to corrupt the context if you did any rendering (other than glClear) while the view wasn't attached, and caused the same GL_FRAMEBUFFER_UNDEFINED error.
The easiest way to solve this, i found, was to assign the NSOpenGLView to its NSOpenGLContext after creation like this:
NSOpenGLView *view = ...;
view.openglContext.view = view;
I'm baffled that, apparently, it's necessary to do this even though the NSOpenGLContext is created by the NSOpenGLView, but there it is.
The trick is to open the View Effects inspector and uncheck the parent View in the Core Animation Layer section.
What is the appropriate way to get rearrangeObjects to be sent to an NSTreeController after changes to nodes in the tree? I have a sample application (full code below) using an NSOutlineView and NSTreeController with a simple tree of Node objects.
In Version1 of the app, when you edit the name of a Node, the tree doesn't get resorted until you click the column header or use the “Rearrange” item in the menu. The latter is set up to directly send rearrangeObjects to the NSTreeController.
In Version2, I tried sending rearrangeObjects from the Node's setName: method. This doesn't seem like a good solution because it means the model now has knowledge of the view/controller. Also, it has the side effect that the outline view loses focus after the rename (if you select a Node and edit its name, the selection bar turns from blue to gray) for some reason (why is that?).
NSArrayController has a method setAutomaticallyRearrangesObjects: but NSTreeController does not? So what is the appropriate way to solve this?
/* example.m
Compile version 1:
gcc -framework Cocoa -o Version1 example.m
Compile version 2:
gcc -framework Cocoa -o Version2 -D REARRANGE_FROM_SETNAME example.m
*/
#import <Cocoa/Cocoa.h>
NSTreeController *treeController;
NSOutlineView *outlineView;
NSScrollView *scrollView;
#interface Node : NSObject {
NSString *name;
NSArray *children;
}
#end
#implementation Node
- (id) initWithName: (NSString*) theName children: (id) theChildren
{
if (self = [super init]) {
name = [theName retain];
children = [theChildren retain];
}
return self;
}
- (void) setName: (NSString*) new
{
[name autorelease];
name = [new retain];
#ifdef REARRANGE_FROM_SETNAME
[treeController rearrangeObjects];
#endif
}
#end
NSArray *createSortDescriptors()
{
return [NSArray arrayWithObject: [NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES]];
}
void createTheTreeController()
{
Node *childNode1 = [[[Node alloc] initWithName:#"B" children:[NSArray array]] autorelease];
Node *childNode2 = [[[Node alloc] initWithName:#"C" children:[NSArray array]] autorelease];
Node *childNode3 = [[[Node alloc] initWithName:#"D" children:[NSArray array]] autorelease];
Node *topNode1 = [[[Node alloc] initWithName:#"A" children:[NSArray arrayWithObjects:childNode1,childNode2,childNode3,nil]] autorelease];
Node *topNode2 = [[[Node alloc] initWithName:#"E" children:[NSArray array]] autorelease];
Node *topNode3 = [[[Node alloc] initWithName:#"F" children:[NSArray array]] autorelease];
NSArray *topNodes = [NSArray arrayWithObjects:topNode1,topNode2,topNode3,nil];
treeController = [[[NSTreeController alloc] initWithContent:topNodes] autorelease];
[treeController setAvoidsEmptySelection:NO];
[treeController setChildrenKeyPath:#"children"];
[treeController setSortDescriptors:createSortDescriptors()];
}
void createTheOutlineView()
{
outlineView = [[[NSOutlineView alloc] initWithFrame:NSMakeRect(0, 0, 284, 200)] autorelease];
[outlineView bind:#"content" toObject:treeController withKeyPath:#"arrangedObjects" options:nil];
[outlineView bind:#"sortDescriptors" toObject:treeController withKeyPath:#"sortDescriptors" options:nil];
[outlineView bind:#"selectionIndexPaths" toObject:treeController withKeyPath:#"selectionIndexPaths" options:nil];
NSTableColumn *column = [[[NSTableColumn alloc] initWithIdentifier:#"NameColumn"] autorelease];
[[column headerCell] setStringValue:#"Name"];
[outlineView addTableColumn:column];
[outlineView setOutlineTableColumn:column];
[column bind:#"value" toObject:treeController withKeyPath:#"arrangedObjects.name" options:nil];
[column setWidth:250];
scrollView = [[[NSScrollView alloc] initWithFrame:NSMakeRect(0, 0, 300, 200)] autorelease];
[scrollView setDocumentView:outlineView];
[scrollView setHasVerticalScroller:YES];
}
void createTheWindow()
{
id window = [[[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 300, 200)
styleMask:NSTitledWindowMask backing:NSBackingStoreBuffered defer:NO]
autorelease];
[window cascadeTopLeftFromPoint:NSMakePoint(20,20)];
[window setTitle:#"Window"];
[window makeKeyAndOrderFront:nil];
[[window contentView] addSubview:scrollView];
}
void createTheMenuBar()
{
id menubar = [[NSMenu new] autorelease];
id appMenuItem = [[NSMenuItem new] autorelease];
[menubar addItem:appMenuItem];
[NSApp setMainMenu:menubar];
id appMenu = [[NSMenu new] autorelease];
#ifndef REARRANGE_FROM_SETNAME
id rearrangeMenuItem = [[[NSMenuItem alloc] initWithTitle:#"Rearrange"
action:#selector(rearrangeObjects) keyEquivalent:#"r"] autorelease];
[rearrangeMenuItem setTarget: treeController];
[appMenu addItem:rearrangeMenuItem];
#endif
id quitMenuItem = [[[NSMenuItem alloc] initWithTitle:#"Quit"
action:#selector(terminate:) keyEquivalent:#"q"] autorelease];
[appMenu addItem:quitMenuItem];
[appMenuItem setSubmenu:appMenu];
}
void setUpAutoReleasePoolAndApplication()
{
[NSAutoreleasePool new];
[NSApplication sharedApplication];
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
}
void activateAppAndRun()
{
[NSApp activateIgnoringOtherApps:YES];
[NSApp run];
}
int main(int argc, const char * argv[])
{
setUpAutoReleasePoolAndApplication();
createTheTreeController();
createTheOutlineView();
createTheWindow();
createTheMenuBar();
activateAppAndRun();
return 0;
}
I'm at least able to partly answer my own question after having looked at Apple's iSpend sample application. Their file TransactionsController_Sorting.m includes a method scheduleRearrangeObjects that invokes rearrangeObjects in a different way. Changing my own code in the same way means including this snippet in the setName: method:
#ifdef REARRANGE_FROM_SETNAME
// Commented out: [treeController rearrangeObjects];
[treeController performSelector:#selector(rearrangeObjects) withObject:nil afterDelay:0.0];
#endif
With this change, the outline view no longer loses focus after renaming a node. What's left to do now is take this code out of the model and into the view/controller; TransactionsController_Sorting seems to also illustrate how to do that. (I still don't understand why the above change prevents the outline view from losing focus though, anyone have an explanation?)
Another answer, as a possible explanation
I believe rearrangeObjects and fetch are delayed until the next runloop iteration. fetch at least tells you so in the docs:
Special Considerations
Beginning with OS X v10.4 the result of this method is deferred until the next iteration of the runloop so that the error presentation mechanism can provide feedback as a sheet.
In my own experimentation, I can use dispatch_async after rearrangeObjects to get code executed after the rearrange. In other words, if I don't dispatch_async code following rearrangeObjects it will be applied before the deferred rearrange. It's a great way to tear out your hair.
In any event, I suspect you were losing focus because rearrangeObjects blows away the context in which you were editing a node as it reloads the whole object tree, but if you force it to execute immediately, you don't lose that context.
[edit] Update here. I was dealing with rearrangeObjects and core data not seeming synchronous, and sure enough, it isn't. I caught an arrayController calling dispatch_async through a binding stack trace.
I'd like to adjust the NSApplicationIcon image that gets shown automatically in all alerts to be something different than what is in the app bundle.
I know that it's possible to set the dock icon with [NSApplication setApplicationIconImage:] -- but this only affects the dock, and nothing else.
I'm able to work around this issue some of the time: I have an NSAlert *, I can call setIcon: to display my alternate image.
Unfortunately, I have a lot of nibs that have NSImageView's with NSApplicationIcon, that I would like to affect, and it would be a hassle to create outlets and put in code to change the icon. And for any alerts that I'm bringing up with the BeginAlert... type calls (which don't give an NSAlert object to muck with), I'm completely out of luck.
Can anybody think of a reasonable way to globally (for the life of a running application) override the NSApplicationIcon that is used by AppKit, with my own image, so that I can get 100% of the alerts replaced (and make my code simpler)?
Swizzle the [NSImage imageNamed:] method? This method works at least on Snow Leopard, YMMV.
In an NSImage category:
#implementation NSImage (Magic)
+ (void)load {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// have to call imageNamed: once prior to swizzling to avoid infinite loop
[[NSApplication sharedApplication] applicationIconImage];
// swizzle!
NSError *error = nil;
if (![NSImage jr_swizzleClassMethod:#selector(imageNamed:) withClassMethod:#selector(_sensible_imageNamed:) error:&error])
NSLog(#"couldn't swizzle imageNamed: application icons will not update: %#", error);
[pool release];
}
+ (id)_sensible_imageNamed:(NSString *)name {
if ([name isEqualToString:#"NSApplicationIcon"])
return [[NSApplication sharedApplication] applicationIconImage];
return [self _sensible_imageNamed:name];
}
#end
With this hacked up (untested, just wrote it) jr_swizzleClassMethod:... implementation:
+ (BOOL)jr_swizzleClassMethod:(SEL)origSel_ withClassMethod:(SEL)altSel_ error:(NSError**)error_ {
#if OBJC_API_VERSION >= 2
Method origMethod = class_getClassMethod(self, origSel_);
if (!origMethod) {
SetNSError(error_, #"original method %# not found for class %#", NSStringFromSelector(origSel_), [self className]);
return NO;
}
Method altMethod = class_getClassMethod(self, altSel_);
if (!altMethod) {
SetNSError(error_, #"alternate method %# not found for class %#", NSStringFromSelector(altSel_), [self className]);
return NO;
}
id metaClass = objc_getMetaClass(class_getName(self));
class_addMethod(metaClass,
origSel_,
class_getMethodImplementation(metaClass, origSel_),
method_getTypeEncoding(origMethod));
class_addMethod(metaClass,
altSel_,
class_getMethodImplementation(metaClass, altSel_),
method_getTypeEncoding(altMethod));
method_exchangeImplementations(class_getClassMethod(self, origSel_), class_getClassMethod(self, altSel_));
return YES;
#else
assert(0);
return NO;
#endif
}
Then, this method to illustrate the point:
- (void)doMagic:(id)sender {
static int i = 0;
i = (i+1) % 2;
if (i)
[[NSApplication sharedApplication] setApplicationIconImage:[NSImage imageNamed:NSImageNameBonjour]];
else
[[NSApplication sharedApplication] setApplicationIconImage:[NSImage imageNamed:NSImageNameDotMac]];
// any pre-populated image views have to be set to nil first, otherwise their icon won't change
// [imageView setImage:nil];
// [imageView setImage:[NSImage imageNamed:NSImageNameApplicationIcon]];
NSAlert *alert = [[[NSAlert alloc] init] autorelease];
[alert setMessageText:#"Shazam!"];
[alert runModal];
}
A couple of caveats:
Any image view already created must have setImage: called twice, as seen above to register the image changing. Don't know why.
There may be a better way to force the initial imageNamed: call with #"NSApplicationIcon" than how I've done it.
Try [myImage setName:#"NSApplicationIcon"] (after setting it as the application icon image in NSApp).
Note: On 10.6 and later, you can and should use NSImageNameApplicationIcon instead of the string literal #"NSApplicationIcon".