Controller.m

/*
    File: Controller.m
Abstract: Handles UI interaction and retrieves window images.
 Version: 1.1
 
Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple
Inc. ("Apple") in consideration of your agreement to the following
terms, and your use, installation, modification or redistribution of
this Apple software constitutes acceptance of these terms.  If you do
not agree with these terms, please do not use, install, modify or
redistribute this Apple software.
 
In consideration of your agreement to abide by the following terms, and
subject to these terms, Apple grants you a personal, non-exclusive
license, under Apple's copyrights in this original Apple software (the
"Apple Software"), to use, reproduce, modify and redistribute the Apple
Software, with or without modifications, in source and/or binary forms;
provided that if you redistribute the Apple Software in its entirety and
without modifications, you must retain this notice and the following
text and disclaimers in all such redistributions of the Apple Software.
Neither the name, trademarks, service marks or logos of Apple Inc. may
be used to endorse or promote products derived from the Apple Software
without specific prior written permission from Apple.  Except as
expressly stated in this notice, no other rights or licenses, express or
implied, are granted by Apple herein, including but not limited to any
patent rights that may be infringed by your derivative works or by other
works in which the Apple Software may be incorporated.
 
The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
 
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
 
Copyright (C) 2010 Apple Inc. All Rights Reserved.
 
*/
 
#import "Controller.h"
 
@implementation Controller
 
#pragma mark Basic Profiling Tools
// Set to 1 to enable basic profiling. Profiling information is logged to console.
#ifndef PROFILE_WINDOW_GRAB
#define PROFILE_WINDOW_GRAB 0
#endif
 
#if PROFILE_WINDOW_GRAB
#define StopwatchStart() AbsoluteTime start = UpTime()
#define Profile(img) CFRelease(CGDataProviderCopyData(CGImageGetDataProvider(img)))
#define StopwatchEnd(caption) do { Duration time = AbsoluteDeltaToDuration(UpTime(), start); double timef = time < 0 ? time / -1000000.0 : time / 1000.0; NSLog(@"%s Time Taken: %f seconds", caption, timef); } while(0)
#else
#define StopwatchStart()
#define Profile(img)
#define StopwatchEnd(caption)
#endif
 
#pragma mark Utilities
 
// Simple helper to twiddle bits in a uint32_t. 
inline uint32_t ChangeBits(uint32_t currentBits, uint32_t flagsToChange, BOOL setFlags);
inline uint32_t ChangeBits(uint32_t currentBits, uint32_t flagsToChange, BOOL setFlags)
{
    if(setFlags)
    {   // Set Bits
        return currentBits | flagsToChange;
    }
    else
    {   // Clear Bits
        return currentBits & ~flagsToChange;
    }
}
 
-(void)setOutputImage:(CGImageRef)cgImage
{
    if(cgImage != NULL)
    {
        // Create a bitmap rep from the image...
        NSBitmapImageRep *bitmapRep = [[NSBitmapImageRep alloc] initWithCGImage:cgImage];
        // Create an NSImage and add the bitmap rep to it...
        NSImage *image = [[NSImage alloc] init];
        [image addRepresentation:bitmapRep];
        [bitmapRep release];
        // Set the output view to the new NSImage.
        [outputView setImage:image];
        [image release];
    }
    else
    {
        [outputView setImage:nil];
    }
}
 
#pragma mark Window List & Window Image Methods
typedef struct
{
    // Where to add window information
    NSMutableArray * outputArray;
    // Tracks the index of the window when first inserted
    // so that we can always request that the windows be drawn in order.
    int order;
} WindowListApplierData;
 
NSString *kAppNameKey = @"applicationName"; // Application Name & PID
NSString *kWindowOriginKey = @"windowOrigin";   // Window Origin as a string
NSString *kWindowSizeKey = @"windowSize";       // Window Size as a string
NSString *kWindowIDKey = @"windowID";           // Window ID
NSString *kWindowLevelKey = @"windowLevel"; // Window Level
NSString *kWindowOrderKey = @"windowOrder"; // The overall front-to-back ordering of the windows as returned by the window server
 
void WindowListApplierFunction(const void *inputDictionary, void *context);
void WindowListApplierFunction(const void *inputDictionary, void *context)
{
    NSDictionary *entry = (NSDictionary*)inputDictionary;
    WindowListApplierData *data = (WindowListApplierData*)context;
    
    // The flags that we pass to CGWindowListCopyWindowInfo will automatically filter out most undesirable windows.
    // However, it is possible that we will get back a window that we cannot read from, so we'll filter those out manually.
    int sharingState = [[entry objectForKey:(id)kCGWindowSharingState] intValue];
    if(sharingState != kCGWindowSharingNone)
    {
        NSMutableDictionary *outputEntry = [NSMutableDictionary dictionary];
        
        // Grab the application name, but since it's optional we need to check before we can use it.
        NSString *applicationName = [entry objectForKey:(id)kCGWindowOwnerName];
        if(applicationName != NULL)
        {
            // PID is required so we assume it's present.
            NSString *nameAndPID = [NSString stringWithFormat:@"%@ (%@)", applicationName, [entry objectForKey:(id)kCGWindowOwnerPID]];
            [outputEntry setObject:nameAndPID forKey:kAppNameKey];
        }
        else
        {
            // The application name was not provided, so we use a fake application name to designate this.
            // PID is required so we assume it's present.
            NSString *nameAndPID = [NSString stringWithFormat:@"((unknown)) (%@)", [entry objectForKey:(id)kCGWindowOwnerPID]];
            [outputEntry setObject:nameAndPID forKey:kAppNameKey];
        }
        
        // Grab the Window Bounds, it's a dictionary in the array, but we want to display it as a string
        CGRect bounds;
        CGRectMakeWithDictionaryRepresentation((CFDictionaryRef)[entry objectForKey:(id)kCGWindowBounds], &bounds);
        NSString *originString = [NSString stringWithFormat:@"%.0f/%.0f", bounds.origin.x, bounds.origin.y];
        [outputEntry setObject:originString forKey:kWindowOriginKey];
        NSString *sizeString = [NSString stringWithFormat:@"%.0f*%.0f", bounds.size.width, bounds.size.height];
        [outputEntry setObject:sizeString forKey:kWindowSizeKey];
        
        // Grab the Window ID & Window Level. Both are required, so just copy from one to the other
        [outputEntry setObject:[entry objectForKey:(id)kCGWindowNumber] forKey:kWindowIDKey];
        [outputEntry setObject:[entry objectForKey:(id)kCGWindowLayer] forKey:kWindowLevelKey];
        
        // Finally, we are passed the windows in order from front to back by the window server
        // Should the user sort the window list we want to retain that order so that screen shots
        // look correct no matter what selection they make, or what order the items are in. We do this
        // by maintaining a window order key that we'll apply later.
        [outputEntry setObject:[NSNumber numberWithInt:data->order] forKey:kWindowOrderKey];
        data->order++;
        
        [data->outputArray addObject:outputEntry];
    }
}
 
-(void)updateWindowList
{
    // Ask the window server for the list of windows.
    StopwatchStart();
    CFArrayRef windowList = CGWindowListCopyWindowInfo(listOptions, kCGNullWindowID);
    StopwatchEnd("Create Window List");
    
    // Copy the returned list, further pruned, to another list. This also adds some bookkeeping
    // information to the list as well as 
    NSMutableArray * prunedWindowList = [NSMutableArray array];
    WindowListApplierData data = {prunedWindowList, 0};
    CFArrayApplyFunction(windowList, CFRangeMake(0, CFArrayGetCount(windowList)), &WindowListApplierFunction, &data);
    CFRelease(windowList);
    
    // Set the new window list
    [arrayController setContent:prunedWindowList];
}
 
-(CFArrayRef)newWindowListFromSelection:(NSArray*)selection
{
    // Create a sort descriptor array. It consists of a single descriptor that sorts based on the kWindowOrderKey in ascending order
    NSArray * sortDescriptors = [NSArray arrayWithObject:[[[NSSortDescriptor alloc] initWithKey:kWindowOrderKey ascending:YES] autorelease]];
 
    // Next sort the selection based on that sort descriptor array
    NSArray * sortedSelection = [selection sortedArrayUsingDescriptors:sortDescriptors];
 
    // Now we Collect the CGWindowIDs from the sorted selection
    CGWindowID *windowIDs = calloc([sortedSelection count], sizeof(CGWindowID));
    int i = 0;
    for(NSMutableDictionary *entry in sortedSelection)
    {
        windowIDs[i++] = [[entry objectForKey:kWindowIDKey] unsignedIntValue];
    }
    // CGWindowListCreateImageFromArray expect a CFArray of *CGWindowID*, not CGWindowID wrapped in a CF/NSNumber
    // Hence we typecast our array above (to avoid the compiler warning) and use NULL CFArray callbacks
    // (because CGWindowID isn't a CF type) to avoid retain/release.
    CFArrayRef windowIDsArray = CFArrayCreate(kCFAllocatorDefault, (const void**)windowIDs, [sortedSelection count], NULL);
    free(windowIDs);
    
    // And send our new array on it's merry way
    return windowIDsArray;
}
 
-(void)createSingleWindowShot:(CGWindowID)windowID
{
    // Create an image from the passed in windowID with the single window option selected by the user.
    StopwatchStart();
    CGImageRef windowImage = CGWindowListCreateImage(imageBounds, singleWindowListOptions, windowID, imageOptions);
    Profile(windowImage);
    StopwatchEnd("Single Window");
    [self setOutputImage:windowImage];
    CGImageRelease(windowImage);
}
 
-(void)createMultiWindowShot:(NSArray*)selection
{
    // Get the correctly sorted list of window IDs. This is a CFArrayRef because we need to put integers in the array
    // instead of CFTypes or NSObjects.
    CFArrayRef windowIDs = [self newWindowListFromSelection:selection];
    
    // And finally create the window image and set it as our output image.
    StopwatchStart();
    CGImageRef windowImage = CGWindowListCreateImageFromArray(imageBounds, windowIDs, imageOptions);
    Profile(windowImage);
    StopwatchEnd("Multiple Window");
    CFRelease(windowIDs);
    [self setOutputImage:windowImage];
    CGImageRelease(windowImage);
}
 
-(void)createScreenShot
{
    // This just invokes the API as you would if you wanted to grab a screen shot. The equivalent using the UI would be to
    // enable all windows, turn off "Fit Image Tightly", and then select all windows in the list.
    StopwatchStart();
    CGImageRef screenShot = CGWindowListCreateImage(CGRectInfinite, kCGWindowListOptionOnScreenOnly, kCGNullWindowID, kCGWindowImageDefault);
    Profile(screenShot);
    StopwatchEnd("Screenshot");
    [self setOutputImage:screenShot];
    CGImageRelease(screenShot);
}
 
#pragma mark GUI Support
 
-(void)updateImageWithSelection
{
    // Depending on how much is selected either clear the output image
    // set the image based on a single selected window or
    // set the image based on multiple selected windows.
    NSArray *selection = [arrayController selectedObjects];
    if([selection count] == 0)
    {
        [self setOutputImage:NULL];
    }
    else if([selection count] == 1)
    {
        // Single window selected, so use the single window options.
        // Need to grab the CGWindowID to pass to the method.
        CGWindowID windowID = [[[selection objectAtIndex:0] objectForKey:kWindowIDKey] unsignedIntValue];
        [self createSingleWindowShot:windowID];
    }
    else
    {
        // Multiple windows selected, so composite just those windows
        [self createMultiWindowShot:selection];
    }
}
 
enum
{
    // Constants that correspond to the rows in the
    // Single Window Option matrix.
    kSingleWindowAboveOnly = 0,
    kSingleWindowAboveIncluded = 1,
    kSingleWindowOnly = 2,
    kSingleWindowBelowIncluded = 3,
    kSingleWindowBelowOnly = 4,
};
 
// Simple helper that converts the selected row number of the singleWindow NSMatrix 
// to the appropriate CGWindowListOption.
-(CGWindowListOption)singleWindowOption
{
    CGWindowListOption option = 0;
    switch([singleWindow selectedRow])
    {
        case kSingleWindowAboveOnly:
            option = kCGWindowListOptionOnScreenAboveWindow;
            break;
            
        case kSingleWindowAboveIncluded:
            option = kCGWindowListOptionOnScreenAboveWindow | kCGWindowListOptionIncludingWindow;
            break;
            
        case kSingleWindowOnly:
            option = kCGWindowListOptionIncludingWindow;
            break;
            
        case kSingleWindowBelowIncluded:
            option = kCGWindowListOptionOnScreenBelowWindow | kCGWindowListOptionIncludingWindow;
            break;
 
        case kSingleWindowBelowOnly:
            option = kCGWindowListOptionOnScreenBelowWindow;
            break;
            
        default:
            break;
    }
    return option;
}
 
NSString *kvoContext = @"SonOfGrabContext";
-(void)awakeFromNib
{
    // Set the initial list options to match the UI.
    listOptions = kCGWindowListOptionAll;
    listOptions = ChangeBits(listOptions, kCGWindowListOptionOnScreenOnly, [listOffscreenWindows intValue] == NSOffState);
    listOptions = ChangeBits(listOptions, kCGWindowListExcludeDesktopElements, [listDesktopWindows intValue] == NSOffState);
 
    // Set the initial image options to match the UI.
    imageOptions = kCGWindowImageDefault;
    imageOptions = ChangeBits(imageOptions, kCGWindowImageBoundsIgnoreFraming, [imageFramingEffects intValue] == NSOnState);
    imageOptions = ChangeBits(imageOptions, kCGWindowImageShouldBeOpaque, [imageOpaqueImage intValue] == NSOnState);
    imageOptions = ChangeBits(imageOptions, kCGWindowImageOnlyShadows, [imageShadowsOnly intValue] == NSOnState);
    
    // Set initial single window options to match the UI.
    singleWindowListOptions = [self singleWindowOption];
    
    // CGWindowListCreateImage & CGWindowListCreateImageFromArray will determine their image size dependent on the passed in bounds.
    // This sample only demonstrates passing either CGRectInfinite to get an image the size of the desktop
    // or passing CGRectNull to get an image that tightly fits the windows specified, but you can pass any rect you like.
    imageBounds = ([imageTightFit intValue] == NSOnState) ? CGRectNull : CGRectInfinite;
    
    // Register for updates to the selection
    [arrayController addObserver:self forKeyPath:@"selectionIndexes" options:0 context:&kvoContext];
    
    // Make sure the source list window is in front
    [[outputView window] makeKeyAndOrderFront:self];
    [[self window] makeKeyAndOrderFront:self];
 
    // Get the initial window list, and set the initial image, but wait for us to return to the
    // event loop so that the sample's windows will be included in the list as well.
    [self performSelectorOnMainThread:@selector(refreshWindowList:) withObject:self waitUntilDone:NO];
    
    // Default to creating a screen shot. Do this after our return since the previous request
    // to refresh the window list will set it to nothing due to the interactions with KVO.
    [self performSelectorOnMainThread:@selector(createScreenShot) withObject:self waitUntilDone:NO];
}
 
-(void)dealloc
{
    // Remove our KVO notification
    [arrayController removeObserver:self forKeyPath:@"selectionIndexes"];
    [super dealloc];
}
 
 
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if(context == &kvoContext)
    {
    // Find the "Single Window" options control and dynamically enable it based on how many items are selected.
    [singleWindow setEnabled:[[arrayController selectedObjects] count] <= 1];
    
    // Selection has changed, so update the image
    [self updateImageWithSelection];
    }
    else
    {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
 
}
 
#pragma mark Control Actions
 
-(IBAction)toggleOffscreenWindows:(id)sender
{
    listOptions = ChangeBits(listOptions, kCGWindowListOptionOnScreenOnly, [sender intValue] == NSOffState);
    [self updateWindowList];
    [self updateImageWithSelection];
}
 
-(IBAction)toggleDesktopWindows:(id)sender
{
    listOptions = ChangeBits(listOptions, kCGWindowListExcludeDesktopElements, [sender intValue] == NSOffState);
    [self updateWindowList];
    [self updateImageWithSelection];
}
 
-(IBAction)toggleFramingEffects:(id)sender
{
    imageOptions = ChangeBits(imageOptions, kCGWindowImageBoundsIgnoreFraming, [sender intValue] == NSOnState);
    [self updateImageWithSelection];
}
 
-(IBAction)toggleOpaqueImage:(id)sender
{
    imageOptions = ChangeBits(imageOptions, kCGWindowImageShouldBeOpaque, [sender intValue] == NSOnState);
    [self updateImageWithSelection];
}
 
-(IBAction)toggleShadowsOnly:(id)sender
{
    imageOptions = ChangeBits(imageOptions, kCGWindowImageOnlyShadows, [sender intValue] == NSOnState);
    [self updateImageWithSelection];
}
 
-(IBAction)toggleTightFit:(id)sender
{
    imageBounds = ([sender intValue] == NSOnState) ? CGRectNull : CGRectInfinite;
    [self updateImageWithSelection];
}
 
-(IBAction)updateSingleWindowOption:(id)sender
{
    #pragma unused(sender)
    singleWindowListOptions = [self singleWindowOption];
    [self updateImageWithSelection];
}
 
-(IBAction)grabScreenShot:(id)sender
{
    #pragma unused(sender)
    [self createScreenShot];
}
 
-(IBAction)refreshWindowList:(id)sender
{
    #pragma unused(sender)
    // Refreshing the window list combines updating the window list and updating the window image.
    [self updateWindowList];
    [self updateImageWithSelection];
}
 
@end