»
May 06, 2010
»

hi, i make macintosh software.

mininfo-1.gif

Few weeks ago I finished my first Mac application — Saucejas.app. It is small (about 1000 lines of code) app which renders pretty image and video slideshow to secondary display using CoreAnimation.

saucejas-app.png

Application itself doesn’t have notably pretty UI or implementation. It was written basically in one day for use in Saucejas show as a informative & mood-setting background. Here is video featuring Saucejas and previous version of app in background which I wrote earlier in Processing.

Nevertheless it was a really good exercise for me and here are few things I’ve learned:

General tips

  • Use Bindings — they’re great!
  • Objective-C is not Java (I’m still primary a Java person), do not try to organize code in Java style
  • Xcode plist editor is not the easiest to use tool for NSArray of NSDictionaries of 2-3 key-value pairs — consider different format for large lists or create custom editor for plist

Scaling NSImage (proportionally)

NSSize SCProportionalSizeForTargetSize(NSSize size, NSSize target) {
   if(NSEqualSizes(size, target)) {
      return size;
   }

   float widthFactor  = target.width  / size.width;
   float heightFactor = target.height / size.height;

   float scalingFactor;

   if (widthFactor < heightFactor) {
      scalingFactor = widthFactor;
   } else {
      scalingFactor = heightFactor;
   }

   return NSMakeSize(size.width  * scalingFactor, size.height * scalingFactor);
}

@implementation NSImage (SCAdditions)

  - (NSImage *)imageByScalingProportionallyToSize:(NSSize)target
  {
     NSSize size = self.size;

     NSSize proportionalSize = SCProportionalSizeForTargetSize(size, target);
     NSImage *scaledImage = [[NSImage alloc] initWithSize:proportionalSize];
     [scaledImage setCacheMode:NSImageCacheNever];

     [scaledImage lockFocus];
     [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
     [self drawInRect:NSMakeRect(.0f, .0f, proportionalSize.width, proportionalSize.height) 
             fromRect:NSMakeRect(.0f, .0f, size.width, size.height) 
            operation:NSCompositeSourceOver 
             fraction:1.0f];
     [scaledImage unlockFocus];

     return [scaledImage autorelease];
  }

@end
  • Drawing to NSImage can also be done in background thread
  • Cache mode must be NSImageCacheNever otherwise all scaled NSImages are cached
  • Set graphics context interpolation mode to High for better results

    h2. Disabling screensaver
// Periodically called by NSTimer
- (void)disableScreenSaverTimerDidUpdate:(NSTimer *)timer
{
   UpdateSystemActivity(OverallAct);
}

Getting all connected displays

#import <IOKit/graphics/IOGraphicsLib.h>
  
@implementation SCDisplay

+ (NSArray *)displays
{
   NSMutableArray *displays = [NSMutableArray array];
   
   CGDirectDisplayID *onlineDisplays = malloc(
      sizeof(CGDirectDisplayID) * 
      kSCMaxDisplayCount);

   CGDisplayCount displayCount;
   CGDisplayErr listErr = CGGetActiveDisplayList(kSCMaxDisplayCount, 
                                                 onlineDisplays, 
                                                 &displayCount);

   if(listErr == kCGErrorSuccess) {
      for(int i=0; i<displayCount; i++) {
         CGDirectDisplayID displayId = onlineDisplays[i];

         CGRect bounds = CGDisplayBounds(displayId);
         
         NSString *name = nil;
         io_service_t port = CGDisplayIOServicePort(displayId);
         
         CFDictionaryRef infoRef = IODisplayCreateInfoDictionary(port, 
                                   kIODisplayOnlyPreferredName);
         
         if(infoRef != NULL) {
            CFDictionaryRef productNameDictionaryRef = CFDictionaryGetValue(infoRef, 
                                                       CFSTR(kDisplayProductName));
            if(productNameDictionaryRef != NULL) {
               CFIndex count = CFDictionaryGetCount(productNameDictionaryRef);
               if(count > 0) {
                  const void **values = (const void **)malloc (sizeof(void *) * count);
                  CFDictionaryGetKeysAndValues(productNameDictionaryRef, NULL, values);
                  
                  CFTypeRef nameRef = (CFTypeRef)values[0];
                  name = [NSString stringWithString:(NSString *)nameRef];
                  
                  free(values);
               }
            }
            CFRelease(infoRef);
         }
         
         if(!name) {
            name = @"Unknown";
         }
         
         SCDisplay *display = [[SCDisplay alloc] initWithDirectDisplayId:displayId 
                                                                  bounds:bounds 
                                                                    name:name];
         [displays addObject:display];
         [display release];
      }
   } else {
      NSLog(@"%@ %s • CGGetOnlineDisplayList failed: %u", [self className], _cmd, listErr);
   }
   free(onlineDisplays);
   
   return displays;
}

@end

Capturing and releasing a display

// CGDirectDisplayID displayId_; from CGGetActiveDisplayList

// Caputure
CGDisplayErr captureErr = CGDisplayCapture(self.displayId);
if(captureErr == kCGErrorSuccess) {
   self.captured = YES;
} else {
   NSLog(@"%@ %s • CGDisplayCapture failed: %u", [self className], _cmd, captureErr);
}

// Release
CGDisplayErr err = CGDisplayRelease(self.displayId);
if(err == kCGErrorSuccess) {
   self.captured = NO;
   released = YES;
} else {
   NSLog(@"%@ %s • CGDisplayRelease failed: %u", [self className], _cmd, err);
}

Full-screen NSWindow on captured display

CGFloat mainDisplayHeight = [[NSScreen mainScreen] frame].size.height;
CGRect displayBounds = self.display.bounds; // from CGDisplayBounds

// secondary displays are "below" main screen
NSRect rect = NSMakeRect(displayBounds.origin.x, 
                         mainDisplayHeight - displayBounds.size.height, 
                         displayBounds.size.width, 
                         displayBounds.size.height);

NSWindow *window = [[NSWindow alloc] initWithContentRect:rect
                                               styleMask:NSBorderlessWindowMask
                                                 backing:NSBackingStoreBuffered
                                                   defer:NO];

[window setLevel:CGShieldingWindowLevel()];
  • See -[NSScreen frame] & -[NSScreen visibleFrame] maybe they calculate correct rect for window
  • To get NSScreen from displayId I assume -[NSScreen deviceDescription] can be used

References:

 
Internet Explorer 6
Are you serious?