« Scrolling About Box

Posted by Brian Christensen on January 11, 2002 [Feedback (4) & TrackBack (0)]

// Introduction

2002-01-11 changes: Replaced the Carbon GetCurrentKeyModifiers() call with a better Cocoa equivalent (thanks to Andreas Monitzer and Finlay Dobbie for pointing this out).

An application's About Box is relatively important, as it is the place where you get to credit yourself for your hard work. In addition, it provides important information to your users such as the version number and support information. In this tutorial I will show you how to make a simple scrolling About Box and, as an added bonus, how to implement a Secret About Box. Have fun!

Scrolling About Box
Figure 1: This is what the final result will look like.

 

// Let's Get Started

Before we start writing any code, let's get all the Interface Builder steps out of the way. Launch Interface Builder, create a new empty nib file, name it "AboutBox.nib", and add it to your project. (If you don't have an existing project to which you'd like to add a scrolling About Box, simply make a new one in Project Builder.) After you've done that, click on the "Classes" tab and create a subclass of NSObject. Name it something like "AboutBox." Add the following outlets in the attributes inspector: appNameField, copyrightField, creditsField, and versionField. Once that's been completed, create the files for our new class (Classes -> Create Files for AboutBox).

Scrolling About Box
Figure 2: Adding the outlets to the AboutBox class.

Now click the Instances tab, select "File's Owner", and set its class to "AboutBox." It's time to build the interface! Here's a screenshot of the interface I built (you can download the nib file here to make things easier):

IB interface
Figure 3: Building the interface.

If you didn't download the nib file, remember to connect up the outlets to "File's Owner." In addition to that, you also need to connect the "File's Owner" as a delegate of your About Box window.

The last thing we need to do is open the MainMenu.nib file, add a new action method called "showAboutBox:" to the app controller and connect the "About MyApplication" menu item to it. Don't forget to add it to your AppController.h file as well. We're ready to roll!

 

// Scrolling Those Glorified Names

Since we want to get the boring parts out of the way first, add the following code in the appropriate place to your AppController.m file:

#import "AboutBox.h";

- (IBAction)showAboutBox:(id)sender
{
    [[AboutBox sharedInstance] showPanel:sender];
}

And to the AboutBox.h file (not AppController.h):

@interface AboutBox : NSObject
{
    IBOutlet id appNameField;
    IBOutlet id copyrightField;
    IBOutlet id creditsField;
    IBOutlet id versionField;
    NSTimer *scrollTimer;
    float currentPosition;
    float maxScrollHeight;
    NSTimeInterval startTime;
    BOOL restartAtTop;
}

+ (AboutBox *)sharedInstance;
- (IBAction)showPanel:(id)sender;

@end

It's time to start editing the "AboutBox.m" file. The following method returns a so-called "shared instance" of our AboutBox class. In other words, it makes sure that there is only a single AboutBox instance allocated and always returns that single instance for our usage. Why, you may ask? Well, it probably isn't all that useful to have multiple About Box windows open when the user chooses the about command from the application menu. (If you scroll back up to where we edited "AppController.m" you'll see the sharedInstance method in action.) This shared instance technique is also often used for things like Preferences window controllers as well.

#import "AboutBox.h"

@implementation AboutBox

static AboutBox *sharedInstance = nil;

+ (AboutBox *)sharedInstance
{
    return sharedInstance ? sharedInstance : [[self alloc] init];
}

On to our actual init method. It checks to see if there is already a shared instance of the class allocated, and if so, deallocates itself.

- (id)init 
{
    if (sharedInstance) {
        [self dealloc];
    } else {
        sharedInstance = [super init];
    }
    
    return sharedInstance;
}

Below is the method that gets called from our showAboutBox: action method in the app controller .m file. What we do first is check if the appNameField outlet has been connected up yet. If not, it will return nil which means that our AboutBox.nib file wasn't loaded yet. We then know that we need to do some preparatory work first before presenting the window.

- (IBAction)showPanel:(id)sender
{
    if (!appNameField)
    {
        NSWindow *theWindow;
        NSString *creditsPath;
        NSAttributedString *creditsString;
        NSString *appName;
        NSString *versionString;
        NSString *copyrightString;
        NSDictionary *infoDictionary;
        CFBundleRef localInfoBundle;
        NSDictionary *localInfoDict;

First we attempt to load the nib file. If it fails, we beep and log a message to the console. It is important to note here that it might be a good idea to present some type of alert box, since most users don't know to look at the console messages to see what went wrong.

        
        if (![NSBundle loadNibNamed:@"AboutBox" owner:self])
        {
        	// int NSRunCriticalAlertPanel(NSString *title, 
        	//		NSString *msg, NSString *defaultButton, 
        	//		NSString *alternateButton, NSString *otherButton, ...);
        	
            NSLog( @"Failed to load AboutBox.nib" );
            NSBeep();
            return;
        }

For convenience, we get the window and the Info.plist dictionary and put them into variables. However, you could also just nest these method calls in the appropriate places.

        theWindow = [appNameField window];
        
        // Get the info dictionary (Info.plist)
        infoDictionary = [[NSBundle mainBundle] infoDictionary];

Since there is currently no equivalent localInfoDictionary method in NSBundle, we have to use some CoreFoundation functions to get at the InfoPlist.strings file. Luckily the Cocoa/CoreFoundation wizards at Apple have made it fairly easy to do this with so-called "toll-free bridging." This means that we can convert the CFDictionary returned by CFBundleGetLocalInfoDictionary() to an NSDictionary without problems. (Note that CFBundle and NSBundle do not have toll-free bridging, so it is still necessary to use a CFBundleRef below.)

        
        // Get the localized info dictionary (InfoPlist.strings)
        localInfoBundle = CFBundleGetMainBundle();
        localInfoDict = (NSDictionary *)
                        CFBundleGetLocalInfoDictionary( localInfoBundle );

The next section of code is fairly straightforward. We use the various keys in the two info dictionaries (Info.plist and InfoPlist.strings) to get at several attributes we need for the About Box (namely the application name and version number).

        // Setup the app name field
        appName = [localInfoDict objectForKey:@"CFBundleName"];
        [appNameField setStringValue:appName];
        
        // Set the about box window title
        [theWindow setTitle:[NSString stringWithFormat:@"About %@", appName]];
        
        // Setup the version field
        versionString = [infoDictionary objectForKey:@"CFBundleVersion"];
        [versionField setStringValue:[NSString stringWithFormat:@"Version %@", 
                                                          versionString]];

This part is a bit trickier. We grab a file called "Credits.rtf" from the application bundle (if you don't have one yet, either create one or download my sample file here and add it to the "Resources" section of your project). As opposed to the above code bits where we used normal NSStrings, for the actual credits we use an NSAttributedString. This lets us use various font faces, sizes, and colors.

       
        // Setup our credits
        creditsPath = [[NSBundle mainBundle] pathForResource:@"Credits" 
                                             ofType:@"rtf"];

        creditsString = [[NSAttributedString alloc] initWithPath:creditsPath 
                                                    documentAttributes:nil];
        
        [creditsField replaceCharactersInRange:NSMakeRange( 0, 0 ) 
                      withRTF:[creditsString RTFFromRange:
                               NSMakeRange( 0, [creditsString length] ) 
                                             documentAttributes:nil]];

And lastly, we fetch the copyright string, get the maximum scroll point we will be scrolling to, and setup our window.

        
        // Setup the copyright field
        copyrightString = [localInfoDict objectForKey:@"NSHumanReadableCopyright"];
        [copyrightField setStringValue:copyrightString];
        
        // Prepare some scroll info
        maxScrollHeight = [[creditsField string] length];
        
        // Setup the window
        [theWindow setExcludedFromWindowsMenu:YES];
        [theWindow setMenu:nil];
        [theWindow center];
    }

Below we check first if the window isn't visible yet. If not, we want to reset all the scrolling info so that it starts at the beginning again when the window appears. We also prepare the "startTime" by getting the current time and adding two seconds to it. This means that there will be a three second delay before the scrolling starts upon activation of the window.

    
    if (![[appNameField window] isVisible])
    {
        currentPosition = 0;
        restartAtTop = NO;
        startTime = [NSDate timeIntervalSinceReferenceDate] + 3.0;
        [creditsField scrollPoint:NSMakePoint( 0, 0 )];
    }
    
    // Show the window
    [[appNameField window] makeKeyAndOrderFront:nil];
}

Now comes our windowDidBecomeKey: method. This method is called whenever the About Box window is activated. We take this opportunity to create our scrolling timer. It will fire every 25 milliseconds, calling the specified selector in the process.

- (void)windowDidBecomeKey:(NSNotification *)notification
{
    scrollTimer = [NSTimer scheduledTimerWithTimeInterval:1/4 
                           target:self 
                           selector:@selector(scrollCredits:) 
                           userInfo:nil 
                           repeats:YES];
}

The following method is called whenever our window becomes inactive (ie. the user clicks on a different window or switches to a different application). When that happens, we stop the scrolling (or to be more precise, we kill off the timer). This adds a nice touch (IMHO) since the user probably isn't paying attention to the credits anymore anyway.

- (void)windowDidResignKey:(NSNotification *)notification
{
    [scrollTimer invalidate];
}

Last but not least comes the scrollCredits: method, which is called every 25 milliseconds by our timer. First off, we check if we have reached the designated start time yet.

- (void)scrollCredits:(NSTimer *)timer
{
    if ([NSDate timeIntervalSinceReferenceDate] >= startTime)
    {

The code bit below checks if we are starting again at the top. If so, we want to reset the startTime so that it waits another three seconds at the top.

        if (restartAtTop)
        {
            // Reset the startTime
            startTime = [NSDate timeIntervalSinceReferenceDate] + 3.0;
            restartAtTop = NO;
            
            // Set the position
            [creditsField scrollPoint:NSMakePoint( 0, 0 )];
            
            return;
        }

Naturally we don't want to continue merrily scrolling along way past the size of the entire credits, so we check if our current scroll point is past it already. If so, we reset the startTime again (which means it will wait for three seconds at the end of the credits before starting at the top again) as well as the currentPosition. In addition to that, we set restartAtTop to YES so that we can wait another three seconds after jumping back to the top. If we haven't reached the end yet, we scroll down to the designated point. At the end of all this, we also increment the scroll position.

        if (currentPosition >= maxScrollHeight) 
        {
            // Reset the startTime
            startTime = [NSDate timeIntervalSinceReferenceDate] + 3.0;
            
            // Reset the position
            currentPosition = 0;
            restartAtTop = YES;
        }
        else
        {
            // Scroll to the position
            [creditsField scrollPoint:NSMakePoint( 0, currentPosition )];
            
            // Increment the scroll position
            currentPosition += 0.01;
        }
    }
}

@end

That's it. Go get yourself a coke, sit back, build and run the app, and watch your name scroll by in shining lights!

 

// Icing On The Cake

Oh, and there's one more thing... a Secret About Box! Also known as easter eggs, they have had a long tradition in computers, especially in Apple system software and hardware (even the recently introduced iPod features a hidden game of breakout). There is a nice history of easter eggs over at MacKiDo if you're interested in learning more. The easter egg I'll show you how to make here won't be particularly original, just the well-known option-key + about this app combination. It's a fun thing to add to your application nonetheless. Be warned though that if you work at a company that doesn't appreciate Secret About Boxes being added to their applications you'll need to be very clever in hiding them properly so that they get past the QA departement (it's especially appropriate to add easter eggs against management's wishes if they won't even let you properly credit yourself in a standard About Box). If you happen to know some useful tricks I'd be interested in hearing them.

Iguana iguana powersurgius
Figure 4: Iguana iguana powersurgius Secret About Box. Present on all PCI PowerMacs running System 7.5.5 through 7.6.1.

We'll make use of NSApplication's currentEvent and NSEvent's modifierFlags method to check if the option key is down. Scroll down to the showAboutBox: method, and make the following changes:

- (IBAction)showAboutBox:(id)sender
{
    if ([[NSApp currentEvent] modifierFlags] & NSAlternateKeyMask)
    {
        // This is left up to your imagination
    }
    else
        [[AboutBox sharedInstance] showPanel:sender];
}

You're now ready to make a Secret About Box! Perhaps a game of pong or something. You could even go fullscreen with the CoreGraphics API and draw some kind of cool OpenGL demo. Make sure you don't go too far overboard though - it's still more important to fix bugs and implement new features than it is to add an easter egg. :-)

 

// Conclusion

Disclaimer: I will not be held responsible if you get fired for adding an easter egg to your company's software without permission. Seriously though, if you have any questions, comments, or suggestions, please feel free to contact me.


Comments

Where's the nib?

Posted by: Houman S on June 1, 2003 08:52 AM

Sorry about that, the nib and rtf files are now available to download.

Posted by: Brian Christensen on June 30, 2003 06:55 PM

You can of course also easily modify it to handle RTFD credits (with pictures and all) by replacing the setup code with the following:

// Setup our credits
creditsPath = [[NSBundle mainBundle] pathForResource:@"Credits"
ofType:@"rtfd"];

creditsString = [[NSAttributedString alloc] initWithPath:creditsPath
documentAttributes:nil];

[creditsField replaceCharactersInRange:NSMakeRange( 0, 0 )
withRTF:[creditsString RTFDFromRange:
NSMakeRange( 0, [creditsString length] )
documentAttributes:nil]];

Posted by: Lars Næsbye Christensen on December 14, 2003 05:53 PM

SO SORRY! Here's the working version of the RTFD version :o) :

// Setup our credits
creditsPath = [[NSBundle mainBundle] pathForResource:@"Credits"
ofType:@"rtfd"];


creditsString = [[NSAttributedString alloc] initWithPath:creditsPath
documentAttributes:nil];

[creditsField replaceCharactersInRange:NSMakeRange( 0, 0 )
withRTFD:[creditsString RTFDFromRange:
NSMakeRange( 0, [creditsString length] )
documentAttributes:nil]];

Posted by: Lars Næsbye Christensen on December 14, 2003 05:55 PM
Post a comment