Using Cocoa and Xcode 4 to set up a simple OpenGL-based app

Alec Jacobson

September 13, 2011

weblog/

Recently I wanted to start a new OpenGL prototyping application. I imagined that it might grow into something bigger so instead of just using GLUT I wanted to start off using a full-on Cocoa app. This will be handy in the end, because developing with Xcode is tailored for Cocoa apps and since I'm usually the only one running my code having native hooks like the correct keyboard modifiers and file dialogs will be convenient. I'm new to Xcode 4, however, and found it extremely frustrating to get a simple Cocoa app with an OpenGL context up and running. In the end here's what I've done to make a basic OpenGL app in Cocoa with TODO: comments left in all the places where one might want to have application specific code.

Make a new Xcode project.

Choose File > New > New Project ... Then choose Mac OS X > Application > Cocoa Application ... Give your "Product" a name, from here on out I will use "BasicOpenGL" as my product/project name. Leave "Create Document Based Application" unchecked Uncheck "Create a git repository" (optional, of course) At this point you should be able to build and run and see a boring blank window pop up.

Create the BasicOpenGLView class

The layout of xcode 4 is rather annoying. Depending on what "Navigator" your looking at you see different GUI elements and have different access to different things. Maybe this is handy once you're used to it. I am not used to it and it is not handy. Set up the Navigator to view the project file hierarchy: View > Navigators > Project Create a file called BasicOpenGLView.h with the following contents:
#import <OpenGL/gl.h>
#import <Cocoa/Cocoa.h>


///////////////////////////////////////////////////////////////////////////////
// OpenGL error handling
///////////////////////////////////////////////////////////////////////////////
// error reporting as both window message and debugger string
void reportError (char * strError);
// if error dump gl errors to debugger string, return error
GLenum glReportError (void);

@interface BasicOpenGLView : NSOpenGLView
{
  // TODO: add object (pointer) for your OpenGL-based app
  
  // Timer object that can be attached to animationTimer method for time-based
  // animations
  NSTimer* timer;
  // Current time at construction
  CFAbsoluteTime start_time;
  // True only if a redraw is necessary, i.e. scene has changed 
  bool damage;
  // This is a cheesy global variable that allows me to to call certain code
  // only once per OpenGL session, I'm not sure how necessary this is
  bool openGL_initialized;
}

///////////////////////////////////////////////////////////////////////////////
// File IO
///////////////////////////////////////////////////////////////////////////////
// Default open function, called when using File > Open dialog, but also when
// files (of correct type) are dragged to the dock icon 
- (IBAction) openDocument: (id) sender;
// Helper method for openDocument that takes the file name of the given file
// directly so that it can be hooked up to the event of double clicking a file
// in Finder
// Inputs:
//   file_name  string containing path to file to be opened
// Returns:
//   true only on success
- (BOOL) openDocumentFromFileName: (NSString *)file_name;
// Default save function, called when using File > Save as ... dialog
- (IBAction) saveDocumentAs: (id) sender;


///////////////////////////////////////////////////////////////////////////////
// Mouse and Keyboard Input
///////////////////////////////////////////////////////////////////////////////
- (void) keyDown:(NSEvent *)theEvent;
- (void) keyUp:(NSEvent *)theEvent;
- (void) mouseDown:(NSEvent *)theEvent;
- (void) rightMouseDown:(NSEvent *)theEvent;
- (void) otherMouseDown:(NSEvent *)theEvent;
- (void) mouseUp:(NSEvent *)theEvent;
- (void) rightMouseUp:(NSEvent *)theEvent;
- (void) otherMouseUp:(NSEvent *)theEvent;
- (void) mouseMoved:(NSEvent *)theEvent;
- (void) mouseDragged:(NSEvent *)theEvent;
- (void) rightMouseDragged:(NSEvent *)theEvent;
- (void) otherMouseDragged:(NSEvent *)theEvent;
- (void) scrollWheel:(NSEvent *)theEvent;
- (void) viewDidMoveToWindow;
// OpenGL apps like to think of (0,0) being the top left corner, cocoa apps
// think of (0,0) as the bottom left corner. This simple flips the y coordinate
// according to the current height
// Inputs:
//   location  point of click according to Cocoa
// Returns
//   point of click according to with y coordinate flipped
- (NSPoint) flip_y:(NSPoint)location;


///////////////////////////////////////////////////////////////////////////////
// OpenGL
///////////////////////////////////////////////////////////////////////////////
// Called whenever openGL context changes size
- (void) reshape;
// Main display or draw function, called when redrawn
- (void) drawRect:(NSRect)rect;
// Set initial OpenGL state (current context is set)
// called after context is created
- (void) prepareOpenGL;
// this can be a troublesome call to do anything heavyweight, as it is called
// on window moves, resizes, and display config changes.  So be careful of
// doing too much here.  window resizes, moves and display changes (resize,
// depth and display config change)
- (void) update;


///////////////////////////////////////////////////////////////////////////////
// Animation
///////////////////////////////////////////////////////////////////////////////

// per-window timer function, basic time based animation preformed here
- (void) animationTimer:(NSTimer *)timer;
// Set an instance variable to the current time, so as to keep track of the time
// at which the app was started
- (void) setStartTime;
// Get the time since the start time: elapsed time since app was started
- (CFAbsoluteTime) getElapsedTime;


///////////////////////////////////////////////////////////////////////////////
// Cocoa
///////////////////////////////////////////////////////////////////////////////
- (BOOL) acceptsFirstResponder;
- (BOOL) becomeFirstResponder;
- (BOOL) resignFirstResponder;
- (void) awakeFromNib;
- (void) terminate:(NSNotification *)aNotification;

@end
And also a file called BasicOpenGLView.h with the following contents:
#import "BasicOpenGLView.h"
// For functions like gluErrorString()
#import <OpenGL/glu.h>
#ifdef __APPLE__
#define _MACOSX
#endif


void reportError (char * strError)
{
  // Set up a fancy font/display for error messages
  NSMutableDictionary *attribs = [NSMutableDictionary dictionary];
  [attribs setObject: [NSFont fontWithName: @"Monaco" size: 9.0f] 
    forKey: NSFontAttributeName];
  [attribs setObject: [NSColor whiteColor] 
    forKey: NSForegroundColorAttributeName];
  // Build the error message string
  NSString * errString = [NSString stringWithFormat:@"Error: %s.", strError];
  // Display to log
  NSLog (@"%@\n", errString);
}

GLenum glReportError (void)
{
  // Get current OpenGL error flag
  GLenum err = glGetError();
  // If there's an error report it
  if (GL_NO_ERROR != err)
  {
    reportError ((char *) gluErrorString (err));
  }
  return err;
}

@implementation BasicOpenGLView

  -(IBAction) openDocument: (id) sender
  {
    NSOpenPanel *tvarNSOpenPanelObj  = [NSOpenPanel openPanel];
    // TODO: Add a item to this list corresponding to each file type extension
    // this app supports opening
    // Create an array of strings specifying valid extensions and HFS file types.
    NSArray *fileTypes = [NSArray arrayWithObjects:
      @"obj",
      @"OBJ",
      NSFileTypeForHFSTypeCode('TEXT'),
      nil];
    // Create an Open file... dialog
    NSInteger tvarNSInteger = [tvarNSOpenPanelObj runModalForTypes:fileTypes];
    // If the user selected OK then load the file
    if(tvarNSInteger == NSOKButton)
    {
      // Pass on file name to opener helper
      [self openDocumentFromFileName:[tvarNSOpenPanelObj filename]];
    }
  }

  - (BOOL)openDocumentFromFileName:(NSString *) file_name
  {
    // convert cocoa string to c string
    const char * c_file_name = [file_name UTF8String];
    // TODO: handle loading a file from filename
    NSLog(@"Opening file: %s", c_file_name);
    damage = true;
    return false;
  }

  -(IBAction) saveDocumentAs: (id) sender
  {
    NSSavePanel *savePanel = [NSSavePanel savePanel]; 
    [savePanel setTitle:@"Save as (.obj by default)"];
    // TODO: Add a item to this list corresponding to each file type extension
    // this app supports opening
    // Create an array of strings specifying valid extensions and HFS file types.
    NSArray *fileTypes = [NSArray arrayWithObjects:
      @"obj",
      @"OBJ",
      NSFileTypeForHFSTypeCode('TEXT'),
      nil];
    // Only allow these file types
    [savePanel setAllowedFileTypes:fileTypes]; 
    [savePanel setTreatsFilePackagesAsDirectories:NO]; 
    // Allow user to save file as he likes
    [savePanel setAllowsOtherFileTypes:YES];
    // Create save as... dialog
    NSInteger user_choice =  
      [savePanel runModalForDirectory:NSHomeDirectory() file:@""];
    // If user selected OK then save the file
    if(NSOKButton == user_choice)
    {
      // convert cocoa string to c string
      const char * file_name = [[savePanel filename] UTF8String];
      // TODO: handle saving default file
      NSLog(@"Saving file to %s", file_name);
    } 
  }

  -(void)keyDown:(NSEvent *)theEvent
  {
    // NOTE: holding a key on the keyboard starts to signal multiple down
    // events (the only one final up event)
    NSString *characters = [theEvent characters];
    if ([characters length])
    {
      // convert characters to single char
      char character = [characters characterAtIndex:0];
      // TODO: Handle key down event
      NSLog(@"Keyboard down: %c\n",character);
    }
    damage = true;
  }

  -(void)keyUp:(NSEvent *)theEvent
  {
    NSString *characters = [theEvent characters];
    if ([characters length])
    {
      // convert characters to single char
      char character = [characters characterAtIndex:0];
      // TODO: Handle key up event
      NSLog(@"Keyboard up: %c\n",character);
    }
    damage = true;
  }

  - (void)mouseDown:(NSEvent *)theEvent
  {
    // Get location of the click
    NSPoint location = 
      [self flip_y:
        [self convertPoint:[theEvent locationInWindow] fromView:nil]];
    // TODO: Handle mouse up event
    NSLog(@"Mouse down at (%g,%g)\n",location.x,location.y);
    damage = true;
  }

  - (void)rightMouseDown:(NSEvent *)theEvent
  {
    // TODO: Handle right mouse button down event
    // For now just treat as left mouse button down event
    [self mouseDown: theEvent];
  }

  - (void)otherMouseDown:(NSEvent *)theEvent
  {
    // TODO: Handle other strange mouse button bown events
    // For now just treat as left mouse button down event
    [self mouseDown: theEvent];
  }

  - (void)mouseUp:(NSEvent *)theEvent
  {
    // Get location of the click
    NSPoint location = 
      [self flip_y:
        [self convertPoint:[theEvent locationInWindow] fromView:nil]];
    // TODO: Handle mouse up event
    NSLog(@"Mouse up at (%g,%g)\n",location.x,location.y);
    damage = true;
  }

  - (void)rightMouseUp:(NSEvent *)theEvent
  {  
    // TODO: Handle right mouse button up event
    // For now just treat as left mouse button up event
    [self mouseUp: theEvent];
  }

  - (void)otherMouseUp:(NSEvent *)theEvent
  {
    // TODO: Handle other strange mouse button up events  
    // For now just treat as left mouse button up event
    [self mouseUp: theEvent];
  }

  - (void)mouseMoved:(NSEvent *)theEvent
  {
    NSPoint location = 
      [self flip_y:
        [self convertPoint:[theEvent locationInWindow] fromView:nil]];
    // TODO: Handle mouse move event
    NSLog(@"Mouse moved to (%g,%g)\n",location.x,location.y);
    damage = true;
  }

  - (void)mouseDragged:(NSEvent *)theEvent
  {
    
    NSPoint location = 
      [self flip_y:
        [self convertPoint:[theEvent locationInWindow] fromView:nil]];
    // TODO: Handle mouse drag event
    NSLog(@"Mouse dragged to (%g,%g)\n",location.x,location.y);
    damage = true;
  }

  - (void)rightMouseDragged:(NSEvent *)theEvent
  { 
    // TODO: Handle right mouse button drag event
    // For now just treat as left mouse button drag event
    [self mouseDragged: theEvent];
  }

  - (void)otherMouseDragged:(NSEvent *)theEvent
  {
    // TODO: Handle other strange mouse button drag event
    // For now just treat as left mouse button drag event
    [self mouseDragged: theEvent];
  }


  - (void)scrollWheel:(NSEvent *)theEvent
  {

    NSPoint location = 
      [self flip_y:
        [self convertPoint:[theEvent locationInWindow] fromView:nil]];
    // TODO: Handle mouse scroll event
    NSLog(@"Mouse scroll wheel at (%g,%g) by (%g,%g)\n",
      location.x,location.y,[theEvent deltaX],[theEvent deltaY]);
    damage = true;
  }

  - (void) viewDidMoveToWindow
  {
    // Listen to all mouse move events (not just dragging)
    [[self window] setAcceptsMouseMovedEvents:YES];
    // When view changes to this window then be sure that we start responding
    // to mouse events
    [[self window] makeFirstResponder:self];
  }

  - (NSPoint) flip_y:(NSPoint) location
  {
    // Get openGL context size
    NSRect rectView = [self bounds];
    // Cocoa gives opposite of OpenGL y direction, flip y direction
    location.y = rectView.size.height - location.y;
    return location;
  }

  - (void) reshape
  {
    NSRect rectView = [self bounds];
    // TODO: Handle resize window using the following
    NSLog(@"New context size: %g %g\n",
      rectView.size.width,rectView.size.height);
  }

  - (void) drawRect:(NSRect)rect
  {
    // TODO: handle draw event
    // For now just clear the screen with a time dependent color
    glClearColor(
      fabs(sin([self getElapsedTime])),
      fabs(sin([self getElapsedTime]/3)),
      fabs(sin([self getElapsedTime]/7)),
      0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    // Elapsed time in seconds: getElapsedTime()
    // Report any OpenGL errors
    glReportError ();
    // Flush all OpenGL calls
    glFlush();
    // Flush OpenGL context
    [[self openGLContext] flushBuffer];
  }

  - (void) prepareOpenGL
  {
    const GLint swapInt = 1;
    // set to vbl sync
    [[self openGLContext] setValues:&swapInt 
      forParameter:NSOpenGLCPSwapInterval];
    if(!openGL_initialized)
    {
      // Get command line arguments and find whether stealFocus is set to YES
      NSUserDefaults *args = [NSUserDefaults standardUserDefaults];
      // also find out if app should steal focus
      bool stealFocus = [args boolForKey:@"stealFocus"];
      if(stealFocus)
      {
        // Steal focus means that the apps window will appear in front of all
        // other programs when it launches even in front of the calling
        // application (e.g. a terminal)
        [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
      }
      // TODO: Initialize OpenGL app, do anything here that you need to *after*
      // the OpenGL context is initialized (load textures, shaders, etc.)
      openGL_initialized = true;
    }
    NSLog(@"prepareOpenGL\n");
  }

  - (void) update 
  {
    [super update];  
  }

  - (void)animationTimer:(NSTimer *)timer
  { 
    // TODO: handle timer based redraw (animation) here
    bool your_app_says_to_redraw = true;
    if(your_app_says_to_redraw || damage)
    {
      damage = false;
      [self drawRect:[self bounds]];
    }
  }

  - (void) setStartTime
  {   
    start_time = CFAbsoluteTimeGetCurrent ();
  }

  - (CFAbsoluteTime) getElapsedTime
  {   
    return CFAbsoluteTimeGetCurrent () - start_time;
  }

  - (BOOL)acceptsFirstResponder
  {
    return YES;
  }

  - (BOOL)becomeFirstResponder
  {
    return  YES;
  }

  - (BOOL)resignFirstResponder
  {
    return YES;
  }

  - (void) awakeFromNib
  {
    openGL_initialized = false;
    // keep track of start/launch time
    [self setStartTime];
    // start animation timer
    timer = [NSTimer timerWithTimeInterval:(1.0f/60.0f) target:self 
      selector:@selector(animationTimer:) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    // ensure timer fires during resize
    [[NSRunLoop currentRunLoop] addTimer:timer 
      forMode:NSEventTrackingRunLoopMode]; 
  }

  - (void) terminate:(NSNotification *)aNotification
  {
    // TODO: delete your app's object
    NSLog(@"Terminating");
  }

@end
This class, BasicOpenGLView, will contain the OpenGL context and hooks (marked with TODO:s and temp code for application specific function calls. For the code to build again we will need to link to OpenGL

Link to OpenGL framework

Set up the Navigator to view the project file hierarchy: View > Navigators > Project Select the Project, and make sure the BasicOpenGL Target is selected. Under Linked Frameworks and Libraries, click the little plus sign and add OpenGL.framework Now the project should build and run again (although it's not actually using our new class)

Hook up BasicOpenGLView class to the application

Change the contents of BasicOpenGLAppDelegate.h to the following:
//
//  BasicOpenGLDelegate.h
//  BasicOpenGL
//
//  Created by Alec Jacobson on 9/13/11.
//  Copyright 2011 New York University. All rights reserved.
//

#import <Cocoa/Cocoa.h>
#import "BasicOpenGLView.h"

@interface BasicOpenGLAppDelegate : NSObject
{
    IBOutlet BasicOpenGLView * basic_opengl_view;
}
// Set the application to terminate when all windows (there is only one) are 
// closed
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:
  (NSApplication *)theApplication;
// Allows the application to open a file on launch and when files are dragged
// to the dock icon of the app
- (BOOL)application:(NSApplication *)theApplication openFile:
  (NSString *)filename;
// Triggered when application is about to terminate
- (void)applicationWillTerminate:(NSNotification *)aNotification;
@end
And the contents of BasicOpenGLAppDelegate.m to:
//
//  BasicOpenGLAppDelegate.m
//  BasicOpenGL
//
//  Created by Alec Jacobson on 9/13/11.
//  Copyright 2011 New York University. All rights reserved.
//

#import "BasicOpenGLAppDelegate.h"

@implementation BasicOpenGLAppDelegate

- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication
{
	return YES;
}
- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename
{ 
  [basic_opengl_view openDocumentFromFileName:filename];
  return YES;
}  
- (void)applicationWillTerminate:(NSNotification *)aNotification
{
  [basic_opengl_view terminate:aNotification];
}

@end
This allows your app to hook up the our class via Cocoa outlets and sets up launching and exiting the program. The code again should build and run, but we still don't see any effect because the class hasn't been added to the GUI window.

Hook up BasicOpenGLView class to GUI

Open up MainMenu.xib
  1. Open View > Utilities > Identity Inspector
  2. Click Window on the right column
  3. Click little cube in bottom right (Object Library)
  4. Drag OpenGL View onto your window
  5. In the View > Utilities > Identity Inspector change NsOpenGLView to the class name to BasicOpenGLView
In the View > Utilities > Connections Inspector, drag a new referencing Outlet to the little blue square in the left column that says "Basic OpenGL App Delegate" At this point you should be able to build and run and see now our window has a rectangle in it changing color over time (this is a result of the default code in BasicOpenGLView.mm actually being executed now). You'll also see that logs are printed to the output concerning keyboard and mouse interactions. However, this colored rectangle, our OpenGL context, is not well placed in the window and does not resize automatically.

Set OpenGL display options and auto-resizing

Still in MainMenu.xib with the openGL context selected. In the View > Utilities > Attributes Inspector change the following: Color: Default Depth: 32 bit Sampling: 16 Samples Anti-aliasing: Default anti-aliasing (Anti aliasing will not show up in the preview unless you reopen the x-code project) In the View > Utilities > Size Inspector you may want to set up default sizes for your window, view and openglview objects, also automatic resizing Select the containing window and in View > Utilities > Attributes Inspector uncheck Memory > "One Shot" (otherwise you'll get a warning at compile time) Now if you build and run you'll see a full-window, opengl display of a changing color.

Recognize certain file types

Currently with the files set up as above, you can "open" and "save" .obj files. Opening and saving does nothing more than print Log statements, but this gives you the idea of what part of the code to edit to implement something interesting. The current application can save through the File >: Save as ... menu item. It can open files through File > Open. But we can also set the app up to open files by dragging .obj files to the application's dock icon or right clicking on a .obj file and selecting BasicOpenGL.app. Actually if you don't have any Mac applications already claiming .obj files you'll see that by just building and running your app all .obj files in finder have been associated with BasicOpenGL.app In the file BasicOpenGLView, replace the default file extension I've used, .obj, in the relevant places with your application specific file type. To add a certain file type to be recognized by your app (so you can drag the file to the dock icon or "Open with ...") then View > Navigators > Project. Select your project, then your target then Info > Document types. In the bottom right choose Add > Add document type. Then give your type a name (3D Object File), an extension (obj) and perhaps an icon (.icns file). Download a working copy of all this as a zip